diff options
Diffstat (limited to 'widget/tests/gtest')
-rw-r--r-- | widget/tests/gtest/MockWinWidget.cpp | 77 | ||||
-rw-r--r-- | widget/tests/gtest/MockWinWidget.h | 85 | ||||
-rw-r--r-- | widget/tests/gtest/TestTimeConverter.cpp | 265 | ||||
-rw-r--r-- | widget/tests/gtest/TestTouchResampler.cpp | 941 | ||||
-rw-r--r-- | widget/tests/gtest/TestWinHeaderOnlyUtils.cpp | 37 | ||||
-rw-r--r-- | widget/tests/gtest/TestWinMessageLoggingUtils.cpp | 101 | ||||
-rw-r--r-- | widget/tests/gtest/TestWinWindowOcclusionTracker.cpp | 167 | ||||
-rw-r--r-- | widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp | 402 | ||||
-rw-r--r-- | widget/tests/gtest/moz.build | 27 |
9 files changed, 2102 insertions, 0 deletions
diff --git a/widget/tests/gtest/MockWinWidget.cpp b/widget/tests/gtest/MockWinWidget.cpp new file mode 100644 index 0000000000..ed7850076f --- /dev/null +++ b/widget/tests/gtest/MockWinWidget.cpp @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "MockWinWidget.h" + +#include "mozilla/gfx/Logging.h" + +NS_IMPL_ISUPPORTS_INHERITED0(MockWinWidget, nsBaseWidget) + +// static +RefPtr<MockWinWidget> MockWinWidget::Create(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect) { + RefPtr<MockWinWidget> window = new MockWinWidget; + if (!window->Initialize(aStyle, aExStyle, aRect)) { + return nullptr; + } + + return window; +} + +MockWinWidget::MockWinWidget() {} + +MockWinWidget::~MockWinWidget() { + if (mWnd) { + ::DestroyWindow(mWnd); + mWnd = 0; + } +} + +bool MockWinWidget::Initialize(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect) { + WNDCLASSW wc; + const wchar_t className[] = L"MozillaMockWinWidget"; + HMODULE hSelf = ::GetModuleHandle(nullptr); + + if (!GetClassInfoW(hSelf, className, &wc)) { + ZeroMemory(&wc, sizeof(WNDCLASSW)); + wc.hInstance = hSelf; + wc.lpfnWndProc = ::DefWindowProc; + wc.lpszClassName = className; + RegisterClassW(&wc); + } + + mWnd = ::CreateWindowExW(aExStyle, className, className, aStyle, aRect.X(), + aRect.Y(), aRect.Width(), aRect.Height(), nullptr, + nullptr, hSelf, nullptr); + if (!mWnd) { + gfxCriticalNoteOnce << "GetClientRect failed " << ::GetLastError(); + return false; + } + + // First nccalcszie (during CreateWindow) for captioned windows is + // deliberately ignored so force a second one here to get the right + // non-client set up. + if (mWnd && (aStyle & WS_CAPTION)) { + ::SetWindowPos(mWnd, NULL, 0, 0, 0, 0, + SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | + SWP_NOACTIVATE | SWP_NOREDRAW); + } + + mBounds = aRect; + return true; +} + +void MockWinWidget::NotifyOcclusionState( + mozilla::widget::OcclusionState aState) { + mCurrentState = aState; +} + +nsSizeMode MockWinWidget::SizeMode() { + if (::IsIconic(mWnd)) { + return nsSizeMode_Minimized; + } + return nsSizeMode_Normal; +} diff --git a/widget/tests/gtest/MockWinWidget.h b/widget/tests/gtest/MockWinWidget.h new file mode 100644 index 0000000000..891e1c942d --- /dev/null +++ b/widget/tests/gtest/MockWinWidget.h @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GTEST_MockWinWidget_H +#define GTEST_MockWinWidget_H + +#include <windows.h> + +#include "nsBaseWidget.h" +#include "Units.h" + +class MockWinWidget : public nsBaseWidget { + public: + static RefPtr<MockWinWidget> Create(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect); + + NS_DECL_ISUPPORTS_INHERITED + + void NotifyOcclusionState(mozilla::widget::OcclusionState aState) override; + + void SetExpectation(mozilla::widget::OcclusionState aExpectation) { + mExpectation = aExpectation; + } + + bool IsExpectingCall() const { return mExpectation != mCurrentState; } + + HWND GetWnd() { return mWnd; } + + nsSizeMode SizeMode() override; + void SetSizeMode(nsSizeMode aMode) override {} + + void* GetNativeData(uint32_t aDataType) override { return nullptr; } + + virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + InitData* aInitData = nullptr) override { + return NS_OK; + } + virtual nsresult Create(nsIWidget* aParent, nsNativeWidget aNativeParent, + const DesktopIntRect& aRect, + InitData* aInitData = nullptr) override { + return NS_OK; + } + virtual void Show(bool aState) override {} + virtual bool IsVisible() const override { return true; } + virtual void Move(double aX, double aY) override {} + virtual void Resize(double aWidth, double aHeight, bool aRepaint) override {} + virtual void Resize(double aX, double aY, double aWidth, double aHeight, + bool aRepaint) override {} + + virtual void Enable(bool aState) override {} + virtual bool IsEnabled() const override { return true; } + virtual void SetFocus(Raise, mozilla::dom::CallerType aCallerType) override {} + virtual void Invalidate(const LayoutDeviceIntRect& aRect) override {} + virtual nsresult SetTitle(const nsAString& title) override { return NS_OK; } + virtual LayoutDeviceIntPoint WidgetToScreenOffset() override { + return LayoutDeviceIntPoint(0, 0); + } + virtual nsresult DispatchEvent(mozilla::WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) override { + return NS_OK; + } + virtual void SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) override {} + virtual InputContext GetInputContext() override { abort(); } + + private: + MockWinWidget(); + ~MockWinWidget(); + + bool Initialize(DWORD aStyle, DWORD aExStyle, + const LayoutDeviceIntRect& aRect); + + HWND mWnd = 0; + + mozilla::widget::OcclusionState mExpectation = + mozilla::widget::OcclusionState::UNKNOWN; + mozilla::widget::OcclusionState mCurrentState = + mozilla::widget::OcclusionState::UNKNOWN; +}; + +#endif diff --git a/widget/tests/gtest/TestTimeConverter.cpp b/widget/tests/gtest/TestTimeConverter.cpp new file mode 100644 index 0000000000..22cbc3f9e6 --- /dev/null +++ b/widget/tests/gtest/TestTimeConverter.cpp @@ -0,0 +1,265 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/TimeStamp.h" +#include "SystemTimeConverter.h" + +using mozilla::SystemTimeConverter; +using mozilla::TimeDuration; +using mozilla::TimeStamp; + +namespace { + +// This class provides a mock implementation of the CurrentTimeGetter template +// type used in SystemTimeConverter. It can be constructed with a particular +// Time and always returns that Time. +template <typename Time> +class MockCurrentTimeGetter { + public: + MockCurrentTimeGetter() : mTime(0) {} + explicit MockCurrentTimeGetter(Time aTime) : mTime(aTime) {} + + // Methods needed for CurrentTimeGetter compatibility + Time GetCurrentTime() const { return mTime; } + void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) {} + + private: + Time mTime; +}; + +// This is another mock implementation of the CurrentTimeGetter template +// type used in SystemTimeConverter, except this asserts that it will not be +// used. i.e. it should only be used in calls to SystemTimeConverter that we +// know will not invoke it. +template <typename Time> +class UnusedCurrentTimeGetter { + public: + Time GetCurrentTime() const { + EXPECT_TRUE(false); + return 0; + } + + void GetTimeAsyncForPossibleBackwardsSkew(const TimeStamp& aNow) { + EXPECT_TRUE(false); + } +}; + +// This class provides a mock implementation of the TimeStampNowProvider +// template type used in SystemTimeConverter. It also has other things in it +// that allow the test to better control time for testing purposes. +class MockTimeStamp { + public: + // This should generally be called at the start of every test function, as + // it will initialize this class's static fields to sane values. In particular + // it will initialize the baseline TimeStamp against which all other + // TimeStamps are compared. + static void Init() { + sBaseline = TimeStamp::Now(); + sTimeStamp = sBaseline; + } + + // Advance the timestamp returned by `MockTimeStamp::Now()` + static void Advance(double ms) { + sTimeStamp += TimeDuration::FromMilliseconds(ms); + } + + // Returns the baseline TimeStamp, that is used as a fixed reference point + // in time against which other TimeStamps can be compared. This is needed + // because mozilla::TimeStamp itself doesn't provide any conversion to + // human-readable strings, and we need to convert it to a TimeDuration in + // order to get that. This baseline TimeStamp can be used to turn an + // arbitrary TimeStamp into a TimeDuration. + static TimeStamp Baseline() { return sBaseline; } + + // This is the method needed for TimeStampNowProvider compatibility, and + // simulates `TimeStamp::Now()` + static TimeStamp Now() { return sTimeStamp; } + + private: + static TimeStamp sTimeStamp; + static TimeStamp sBaseline; +}; + +TimeStamp MockTimeStamp::sTimeStamp; +TimeStamp MockTimeStamp::sBaseline; + +// Could have platform-specific implementations of this using DWORD, guint32, +// etc behind ifdefs. But this is sufficient for now. +using GTestTime = uint32_t; +using TimeConverter = SystemTimeConverter<GTestTime, MockTimeStamp>; + +} // namespace + +// Checks the expectation that the TimeStamp `ts` is exactly `ms` milliseconds +// after the baseline timestamp. This is a macro so gtest still gives us useful +// line numbers for failures. +#define EXPECT_TS(ts, ms) \ + EXPECT_EQ((ts)-MockTimeStamp::Baseline(), TimeDuration::FromMilliseconds(ms)) + +#define EXPECT_TS_FUZZY(ts, ms) \ + EXPECT_DOUBLE_EQ(((ts)-MockTimeStamp::Baseline()).ToMilliseconds(), ms) + +TEST(TimeConverter, SanityCheck) +{ + MockTimeStamp::Init(); + + MockCurrentTimeGetter timeGetter(10); + UnusedCurrentTimeGetter<GTestTime> unused; + TimeConverter converter; + + // This call sets the reference time and timestamp + TimeStamp ts = converter.GetTimeStampFromSystemTime(10, timeGetter); + EXPECT_TS(ts, 0); + + // Advance "TimeStamp::Now" by 10ms, use the same event time and OS time. + // Since the event time is the same as before, we expect to get back the + // same TimeStamp as before too, despite Now() changing. + MockTimeStamp::Advance(10); + ts = converter.GetTimeStampFromSystemTime(10, unused); + EXPECT_TS(ts, 0); + + // Now let's use an event time 20ms after the old event. This will trigger + // forward skew detection and resync the TimeStamp for the new event to Now(). + ts = converter.GetTimeStampFromSystemTime(30, unused); + EXPECT_TS(ts, 10); +} + +TEST(TimeConverter, Overflow) +{ + // This tests wrapping time around the max value supported in the GTestTime + // type and ensuring it is handled properly. + + MockTimeStamp::Init(); + + const GTestTime max = std::numeric_limits<GTestTime>::max(); + const GTestTime min = std::numeric_limits<GTestTime>::min(); + double fullRange = (double)max - (double)min; + double wrapPeriod = fullRange + 1.0; + + GTestTime almostOverflowed = max - 100; + GTestTime overflowed = max + 100; + MockCurrentTimeGetter timeGetter(almostOverflowed); + UnusedCurrentTimeGetter<GTestTime> unused; + TimeConverter converter; + + // Set reference time to 100ms before the overflow point + TimeStamp ts = + converter.GetTimeStampFromSystemTime(almostOverflowed, timeGetter); + EXPECT_TS(ts, 0); + + // Advance everything by 200ms and verify we get back a TimeStamp 200ms from + // the baseline despite wrapping an overflow. + MockTimeStamp::Advance(200); + ts = converter.GetTimeStampFromSystemTime(overflowed, unused); + EXPECT_TS(ts, 200); + + // Advance by another full wraparound of the time. This loses some precision + // so we have to do the FUZZY match + MockTimeStamp::Advance(wrapPeriod); + ts = converter.GetTimeStampFromSystemTime(overflowed, unused); + EXPECT_TS_FUZZY(ts, 200.0 + wrapPeriod); +} + +TEST(TimeConverter, InvertedOverflow) +{ + // This tests time going from near the min value of GTestTime to the max + // value of GTestTime + + MockTimeStamp::Init(); + + const GTestTime max = std::numeric_limits<GTestTime>::max(); + const GTestTime min = std::numeric_limits<GTestTime>::min(); + double fullRange = (double)max - (double)min; + double wrapPeriod = fullRange + 1.0; + + GTestTime nearRangeMin = min + 100; + GTestTime nearRangeMax = max - 100; + double gap = (double)nearRangeMax - (double)nearRangeMin; + + MockCurrentTimeGetter timeGetter(nearRangeMin); + UnusedCurrentTimeGetter<GTestTime> unused; + TimeConverter converter; + + // Set reference time to value near min numeric limit + TimeStamp ts = converter.GetTimeStampFromSystemTime(nearRangeMin, timeGetter); + EXPECT_TS(ts, 0); + + // Advance to value near max numeric limit + MockTimeStamp::Advance(gap); + ts = converter.GetTimeStampFromSystemTime(nearRangeMax, unused); + EXPECT_TS(ts, gap); + + // Advance by another full wraparound of the time. This loses some precision + // so we have to do the FUZZY match + MockTimeStamp::Advance(wrapPeriod); + ts = converter.GetTimeStampFromSystemTime(nearRangeMax, unused); + EXPECT_TS_FUZZY(ts, gap + wrapPeriod); +} + +TEST(TimeConverter, HalfRangeBoundary) +{ + MockTimeStamp::Init(); + + GTestTime max = std::numeric_limits<GTestTime>::max(); + GTestTime min = std::numeric_limits<GTestTime>::min(); + double fullRange = (double)max - (double)min; + double wrapPeriod = fullRange + 1.0; + GTestTime halfRange = (GTestTime)(fullRange / 2.0); + GTestTime halfWrapPeriod = (GTestTime)(wrapPeriod / 2.0); + + TimeConverter converter; + + GTestTime firstEvent = 10; + MockCurrentTimeGetter timeGetter(firstEvent); + UnusedCurrentTimeGetter<GTestTime> unused; + + // Set reference time + TimeStamp ts = converter.GetTimeStampFromSystemTime(firstEvent, timeGetter); + EXPECT_TS(ts, 0); + + // Advance event time by just under the half-period, to trigger about as big + // a forwards skew as we possibly can. + GTestTime secondEvent = firstEvent + (halfWrapPeriod - 1); + ts = converter.GetTimeStampFromSystemTime(secondEvent, unused); + EXPECT_TS(ts, 0); + + // The above forwards skew will have reset the reference timestamp. Now + // advance Now time by just under the half-range, to trigger about as big + // a backwards skew as we possibly can. + MockTimeStamp::Advance(halfRange - 1); + ts = converter.GetTimeStampFromSystemTime(secondEvent, unused); + EXPECT_TS(ts, 0); +} + +TEST(TimeConverter, FractionalMillisBug1626734) +{ + MockTimeStamp::Init(); + + TimeConverter converter; + + GTestTime eventTime = 10; + MockCurrentTimeGetter timeGetter(eventTime); + UnusedCurrentTimeGetter<GTestTime> unused; + + TimeStamp ts = converter.GetTimeStampFromSystemTime(eventTime, timeGetter); + EXPECT_TS(ts, 0); + + MockTimeStamp::Advance(0.2); + ts = converter.GetTimeStampFromSystemTime(eventTime, unused); + EXPECT_TS(ts, 0); + + MockTimeStamp::Advance(0.9); + TimeStamp ts2 = converter.GetTimeStampFromSystemTime(eventTime, unused); + EXPECT_TS(ts2, 0); + + // Since ts2 came from a "future" call relative to ts, we expect ts2 to not + // be "before" ts. (i.e. time shouldn't go backwards, even by fractional + // milliseconds). This assertion is technically already implied by the + // EXPECT_TS checks above, but fixing this assertion is the end result that + // we wanted in bug 1626734 so it feels appropriate to recheck it explicitly. + EXPECT_TRUE(ts <= ts2); +} diff --git a/widget/tests/gtest/TestTouchResampler.cpp b/widget/tests/gtest/TestTouchResampler.cpp new file mode 100644 index 0000000000..1a5b8e2430 --- /dev/null +++ b/widget/tests/gtest/TestTouchResampler.cpp @@ -0,0 +1,941 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <initializer_list> +#include "InputData.h" +#include "Units.h" +#include "gtest/gtest.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "TouchResampler.h" + +using namespace mozilla; +using widget::TouchResampler; + +class TouchResamplerTest : public ::testing::Test { + protected: + virtual void SetUp() { baseTimeStamp = TimeStamp::Now(); } + + TimeStamp Time(double aMilliseconds) { + return baseTimeStamp + TimeDuration::FromMilliseconds(aMilliseconds); + } + + uint64_t ProcessEvent( + MultiTouchInput::MultiTouchType aType, + std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> + aHistoricalData, + const TimeStamp& aTimeStamp, const ScreenIntPoint& aPosition) { + MultiTouchInput input(aType, 0, aTimeStamp, 0); + input.mTouches.AppendElement(SingleTouchData(1, aPosition, {}, 0.0f, 0.0f)); + for (const auto& histData : aHistoricalData) { + input.mTouches[0].mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + histData.first, histData.second, {}, {}, 0.0f, 0.0f}); + } + return resampler.ProcessEvent(std::move(input)); + } + + void CheckTime(const TimeStamp& aTimeStamp, + const TimeStamp& aExpectedTimeStamp) { + EXPECT_EQ((aTimeStamp - baseTimeStamp).ToMilliseconds(), + (aExpectedTimeStamp - baseTimeStamp).ToMilliseconds()); + } + + void CheckEvent(const MultiTouchInput& aEvent, + MultiTouchInput::MultiTouchType aExpectedType, + std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> + aExpectedHistoricalData, + const TimeStamp& aExpectedTimeStamp, + const ScreenIntPoint& aExpectedPosition) { + EXPECT_EQ(aEvent.mType, aExpectedType); + EXPECT_EQ(aEvent.mTouches.Length(), size_t(1)); + EXPECT_EQ(aEvent.mTouches[0].mHistoricalData.Length(), + aExpectedHistoricalData.size()); + for (size_t i = 0; i < aExpectedHistoricalData.size(); i++) { + CheckTime(aEvent.mTouches[0].mHistoricalData[i].mTimeStamp, + aExpectedHistoricalData.begin()[i].first); + EXPECT_EQ(aEvent.mTouches[0].mHistoricalData[i].mScreenPoint, + aExpectedHistoricalData.begin()[i].second); + } + CheckTime(aEvent.mTimeStamp, aExpectedTimeStamp); + EXPECT_EQ(aEvent.mTouches[0].mScreenPoint, aExpectedPosition); + } + + struct ExpectedOutgoingEvent { + Maybe<uint64_t> mEventId; + MultiTouchInput::MultiTouchType mType = MultiTouchInput::MULTITOUCH_START; + std::initializer_list<std::pair<TimeStamp, ScreenIntPoint>> mHistoricalData; + TimeStamp mTimeStamp; + ScreenIntPoint mPosition; + }; + + void CheckOutgoingEvents( + std::initializer_list<ExpectedOutgoingEvent> aExpectedEvents) { + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_EQ(outgoing.size(), aExpectedEvents.size()); + for (const auto& expectedEvent : aExpectedEvents) { + auto outgoingEvent = std::move(outgoing.front()); + outgoing.pop(); + + EXPECT_EQ(outgoingEvent.mEventId, expectedEvent.mEventId); + CheckEvent(outgoingEvent.mEvent, expectedEvent.mType, + expectedEvent.mHistoricalData, expectedEvent.mTimeStamp, + expectedEvent.mPosition); + } + } + + TimeStamp baseTimeStamp; + TouchResampler resampler; +}; + +TEST_F(TouchResamplerTest, BasicExtrapolation) { + // Execute the following sequence: + // + // 0----------10-------16-----20---------------32------------ + // * touchstart at (10, 10) + // * touchmove at (20, 20) + // * frame + // * touchend at (20, 20) + // * frame + // + // And expect the following output: + // + // 0----------10-------16-----20---------------32------------ + // * touchstart at (10, 10) + // * touchmove at (26, 26) + // * touchmove at (20, 20) + // * touchend at (20, 20) + // + // The first frame should emit an extrapolated touchmove from the position + // data in the touchstart and touchmove events. + // The touchend should force a synthesized touchmove that returns back to a + // non-resampled position. + + EXPECT_FALSE(resampler.InTouchingState()); + + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + EXPECT_TRUE(resampler.InTouchingState()); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(20, 20)); + + resampler.NotifyFrame(Time(16.0)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 20)}}, + Time(16.0), + ScreenIntPoint(26, 26)}, + }); + + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(20.0), + ScreenIntPoint(20, 20)); + + EXPECT_FALSE(resampler.InTouchingState()); + + CheckOutgoingEvents({ + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(16.0), + ScreenIntPoint(20, 20)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(20.0), + ScreenIntPoint(20, 20)}, + }); + + // No more events should be produced from here on out. + resampler.NotifyFrame(Time(32.0)); + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_TRUE(outgoing.empty()); +} + +TEST_F(TouchResamplerTest, BasicInterpolation) { + // Same test as BasicExtrapolation, but with a frame time that's 10ms earlier. + // + // Execute the following sequence: + // + // 0------6---10-----------20--22------------30------------- + // * touchstart at (10, 10) + // * touchmove at (20, 20) + // * frame + // * touchend at (20, 20) + // * frame + // + // And expect the following output: + // + // 0------6---10-----------20--22------------30------------- + // * touchstart at (10, 10) + // * touchmove (16, 16) + // * touchmove (20, 20) + // * touchend at (20, 20) + // + // The first frame should emit an interpolated touchmove from the position + // data in the touchstart and touchmove events. + // The touchend should create a touchmove that returns back to a non-resampled + // position. + + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + EXPECT_TRUE(resampler.InTouchingState()); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(20, 20)); + + resampler.NotifyFrame(Time(6.0)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(6.0), + ScreenIntPoint(16, 16)}, + }); + + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(20.0), + ScreenIntPoint(20, 20)); + EXPECT_FALSE(resampler.InTouchingState()); + + CheckOutgoingEvents({ + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(10.0), + ScreenIntPoint(20, 20)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(20.0), + ScreenIntPoint(20, 20)}, + }); + + // No more events should be produced from here on out. + resampler.NotifyFrame(Time(22.0)); + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_TRUE(outgoing.empty()); +} + +TEST_F(TouchResamplerTest, InterpolationFromHistoricalData) { + // Interpolate from the historical data in a touch move event. + // + // Execute the following sequence: + // + // 0----------10-------16-----20-----------30--32------------ + // * touchstart at (10, 10) + // * [hist] at (20, 25) for + // `---------------* touchmove at (30, 30) + // * frame + // * touchend at (30, 30) + // * frame + // + // And expect the following output: + // + // 0----------10-------16-----20-----------30--32------------ + // * touchstart at (10, 10) + // * [hist] at (20, 25) for + // `--------* touchmove at (26, 28) + // * touchmove at (30, 30) + // * touchend at (30, 30) + // + // The first frame should emit an interpolated touchmove from the position + // data in the touchmove event, and integrate the historical data point into + // the resampled event. + // The touchend should force a synthesized touchmove that returns back to a + // non-resampled position. + + // This also tests that interpolation works for both x and y, by giving the + // historical datapoint different values for x and y. + // (26, 28) is 60% of the way from (20, 25) to (30, 30). + + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 25)}}, + Time(20.0), ScreenIntPoint(30, 30)); + resampler.NotifyFrame(Time(16.0)); + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(30.0), + ScreenIntPoint(30, 30)); + resampler.NotifyFrame(Time(32.0)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 25)}}, + Time(16.0), + ScreenIntPoint(26, 28)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(20.0), + ScreenIntPoint(30, 30)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(30.0), + ScreenIntPoint(30, 30)}, + }); +} + +TEST_F(TouchResamplerTest, MultipleTouches) { + EXPECT_FALSE(resampler.InTouchingState()); + + // Touch start + MultiTouchInput inputStart0(MultiTouchInput::MULTITOUCH_START, 0, Time(0.0), + 0); + inputStart0.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(10, 10), {}, 0.0f, 0.0f)); + auto idStart0 = resampler.ProcessEvent(std::move(inputStart0)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch move + MultiTouchInput inputMove1(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(20.0), + 0); + inputMove1.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 30), {}, 0.0f, 0.0f)); + inputMove1.mTouches[0].mHistoricalData.AppendElement( + SingleTouchData::HistoricalTouchData{ + Time(10.0), ScreenIntPoint(20, 25), {}, {}, 0.0f, 0.0f}); + auto idMove1 = resampler.ProcessEvent(std::move(inputMove1)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(16.0)); + + // Touch move + MultiTouchInput inputMove2(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(30.0), + 0); + inputMove2.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 40), {}, 0.0f, 0.0f)); + auto idMove2 = resampler.ProcessEvent(std::move(inputMove2)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch start + MultiTouchInput inputStart3(MultiTouchInput::MULTITOUCH_START, 0, Time(30.0), + 0); + inputStart3.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 10), {}, 0.0f, 0.0f)); + inputStart3.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 40), {}, 0.0f, 0.0f)); + auto idStart3 = resampler.ProcessEvent(std::move(inputStart3)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch move + MultiTouchInput inputMove4(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(40.0), + 0); + inputMove4.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 50), {}, 0.0f, 0.0f)); + inputMove4.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 30), {}, 0.0f, 0.0f)); + auto idMove4 = resampler.ProcessEvent(std::move(inputMove4)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(32.0)); + + // Touch move + MultiTouchInput inputMove5(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(50.0), + 0); + inputMove5.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 60), {}, 0.0f, 0.0f)); + inputMove5.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 40), {}, 0.0f, 0.0f)); + auto idMove5 = resampler.ProcessEvent(std::move(inputMove5)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Touch end + MultiTouchInput inputEnd6(MultiTouchInput::MULTITOUCH_END, 0, Time(50.0), 0); + // Touch point with identifier 1 is lifted + inputEnd6.mTouches.AppendElement( + SingleTouchData(1, ScreenIntPoint(30, 60), {}, 0.0f, 0.0f)); + auto idEnd6 = resampler.ProcessEvent(std::move(inputEnd6)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(48.0)); + + // Touch move + MultiTouchInput inputMove7(MultiTouchInput::MULTITOUCH_MOVE, 0, Time(60.0), + 0); + inputMove7.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 60), {}, 0.0f, 0.0f)); + auto idMove7 = resampler.ProcessEvent(std::move(inputMove7)); + EXPECT_TRUE(resampler.InTouchingState()); + + // Frame + resampler.NotifyFrame(Time(64.0)); + + // Touch end + MultiTouchInput inputEnd8(MultiTouchInput::MULTITOUCH_END, 0, Time(70.0), 0); + // Touch point with identifier 2 is lifted + inputEnd8.mTouches.AppendElement( + SingleTouchData(2, ScreenIntPoint(100, 60), {}, 0.0f, 0.0f)); + auto idEnd8 = resampler.ProcessEvent(std::move(inputEnd8)); + EXPECT_FALSE(resampler.InTouchingState()); + + // Check outgoing events + auto outgoing = resampler.ConsumeOutgoingEvents(); + EXPECT_EQ(outgoing.size(), size_t(9)); + + auto outgoingStart0 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingStart0.mEventId, Some(idStart0)); + CheckEvent(outgoingStart0.mEvent, MultiTouchInput::MULTITOUCH_START, {}, + Time(0.0), ScreenIntPoint(10, 10)); + + auto outgoingMove1 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove1.mEventId, Some(idMove1)); + // (26, 28) is 60% of the way from (20, 25) to (30, 30). + CheckEvent(outgoingMove1.mEvent, MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 25)}}, Time(16.0), + ScreenIntPoint(26, 28)); + + auto outgoingMove2 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove2.mEventId, Some(idMove2)); + CheckEvent(outgoingMove2.mEvent, MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(30, 30)}}, Time(30.0), + ScreenIntPoint(30, 40)); + + auto outgoingStart3 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingStart3.mEventId, Some(idStart3)); + EXPECT_EQ(outgoingStart3.mEvent.mType, MultiTouchInput::MULTITOUCH_START); + CheckTime(outgoingStart3.mEvent.mTimeStamp, Time(30.0)); + EXPECT_EQ(outgoingStart3.mEvent.mTouches.Length(), size_t(2)); + // touch order should be taken from the original touch start event + EXPECT_EQ(outgoingStart3.mEvent.mTouches[0].mIdentifier, 2); + EXPECT_EQ(outgoingStart3.mEvent.mTouches[0].mScreenPoint, + ScreenIntPoint(100, 10)); + EXPECT_EQ(outgoingStart3.mEvent.mTouches[1].mIdentifier, 1); + EXPECT_EQ(outgoingStart3.mEvent.mTouches[1].mScreenPoint, + ScreenIntPoint(30, 40)); + + auto outgoingMove4 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove4.mEventId, Some(idMove4)); + EXPECT_EQ(outgoingMove4.mEvent.mType, MultiTouchInput::MULTITOUCH_MOVE); + CheckTime(outgoingMove4.mEvent.mTimeStamp, Time(32.0)); + EXPECT_EQ(outgoingMove4.mEvent.mTouches.Length(), size_t(2)); + // Touch order should be taken from the original touch move event. + // Both touches should be resampled. + EXPECT_EQ(outgoingMove4.mEvent.mTouches[0].mIdentifier, 1); + EXPECT_EQ(outgoingMove4.mEvent.mTouches[0].mScreenPoint, + ScreenIntPoint(30, 42)); + EXPECT_EQ(outgoingMove4.mEvent.mTouches[1].mIdentifier, 2); + EXPECT_EQ(outgoingMove4.mEvent.mTouches[1].mScreenPoint, + ScreenIntPoint(100, 14)); + + auto outgoingMove5 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove5.mEventId, Some(idMove5)); + EXPECT_EQ(outgoingMove5.mEvent.mType, MultiTouchInput::MULTITOUCH_MOVE); + CheckTime(outgoingMove5.mEvent.mTimeStamp, Time(50.0)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches.Length(), size_t(2)); + // touch order should be taken from the original touch move event + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mIdentifier, 1); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mScreenPoint, + ScreenIntPoint(30, 60)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mHistoricalData.Length(), + size_t(1)); + CheckTime(outgoingMove5.mEvent.mTouches[0].mHistoricalData[0].mTimeStamp, + Time(40.0)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[0].mHistoricalData[0].mScreenPoint, + ScreenIntPoint(30, 50)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mIdentifier, 2); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mScreenPoint, + ScreenIntPoint(100, 40)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mHistoricalData.Length(), + size_t(1)); + CheckTime(outgoingMove5.mEvent.mTouches[1].mHistoricalData[0].mTimeStamp, + Time(40.0)); + EXPECT_EQ(outgoingMove5.mEvent.mTouches[1].mHistoricalData[0].mScreenPoint, + ScreenIntPoint(100, 30)); + + auto outgoingEnd6 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingEnd6.mEventId, Some(idEnd6)); + CheckEvent(outgoingEnd6.mEvent, MultiTouchInput::MULTITOUCH_END, {}, + Time(50.0), ScreenIntPoint(30, 60)); + + auto outgoingMove7 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingMove7.mEventId, Some(idMove7)); + // No extrapolation because the frame at 64.0 cleared the data points because + // there was no pending touch move event at that point + CheckEvent(outgoingMove7.mEvent, MultiTouchInput::MULTITOUCH_MOVE, {}, + Time(60.0), ScreenIntPoint(100, 60)); + EXPECT_EQ(outgoingMove7.mEvent.mTouches[0].mIdentifier, 2); + + auto outgoingEnd8 = std::move(outgoing.front()); + outgoing.pop(); + EXPECT_EQ(outgoingEnd8.mEventId, Some(idEnd8)); + CheckEvent(outgoingEnd8.mEvent, MultiTouchInput::MULTITOUCH_END, {}, + Time(70.0), ScreenIntPoint(100, 60)); +} + +TEST_F(TouchResamplerTest, MovingPauses) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(10, 10)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(20, 20)); + resampler.NotifyFrame(Time(16.0)); + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(30.0), + ScreenIntPoint(40, 40)); + resampler.NotifyFrame(Time(32.0)); + auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0), + ScreenIntPoint(50, 40)); + resampler.NotifyFrame(Time(48.0)); + resampler.NotifyFrame(Time(64.0)); + auto idEnd4 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(50, 40)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(10, 10)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(20, 20)}}, + Time(16.0), + ScreenIntPoint(26, 26)}, + + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(30.0), ScreenIntPoint(40, 40)}}, + Time(32.0), + ScreenIntPoint(42, 42)}, + + {Some(idMove3), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(40.0), ScreenIntPoint(50, 40)}}, + Time(48.0), + ScreenIntPoint(58, 40)}, + + // There was no event between two frames here, so we expect a reset event, + // so that we pause at a non-resampled position. + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(48.0), + ScreenIntPoint(50, 40)}, + + {Some(idEnd4), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(50, 40)}, + }); +} + +TEST_F(TouchResamplerTest, MixedInterAndExtrapolation) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(0, 10)); + resampler.NotifyFrame(Time(11.0)); // 16 - 5 + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, Time(30.0), + ScreenIntPoint(0, 30)); + resampler.NotifyFrame(Time(27.0)); // 32 - 5 + auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0), + ScreenIntPoint(0, 40)); + resampler.NotifyFrame(Time(43.0)); // 48 - 5 + auto idMove4 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(50.0), ScreenIntPoint(0, 50)}}, Time(60.0), + ScreenIntPoint(0, 60)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd5 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(11.0), + ScreenIntPoint(0, 11)}, + + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, + Time(27.0), + ScreenIntPoint(0, 27)}, + + {Some(idMove3), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(30.0), ScreenIntPoint(0, 30)}, + {Time(40.0), ScreenIntPoint(0, 40)}}, + Time(43.0), + ScreenIntPoint(0, 43)}, + + {Some(idMove4), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(50.0), ScreenIntPoint(0, 50)}}, + Time(59.0), + ScreenIntPoint(0, 59)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(60.0), + ScreenIntPoint(0, 60)}, + + {Some(idEnd5), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, MultipleMoveEvents) { + // Test what happens if multiple touch move events appear between two frames. + // This scenario shouldn't occur on Android but we should be able to deal with + // it anyway. Check that we don't discard any event IDs. + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(10.0), + ScreenIntPoint(0, 10)); + resampler.NotifyFrame(Time(11.0)); // 16 - 5 + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, Time(30.0), + ScreenIntPoint(0, 30)); + auto idMove3 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(40.0), + ScreenIntPoint(0, 40)); + auto idMove4 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(45.0), ScreenIntPoint(0, 45)}}, Time(50.0), + ScreenIntPoint(0, 50)); + auto idMove5 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, {}, Time(55.0), + ScreenIntPoint(0, 55)); + resampler.NotifyFrame(Time(43.0)); // 48 - 5 + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd5 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(11.0), + ScreenIntPoint(0, 11)}, + + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, + Time(30.0), + ScreenIntPoint(0, 30)}, + + {Some(idMove3), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(40.0), + ScreenIntPoint(0, 40)}, + + {Some(idMove4), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(43.0), + ScreenIntPoint(0, 43)}, + + {Some(idMove5), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(45.0), ScreenIntPoint(0, 45)}, + {Time(50.0), ScreenIntPoint(0, 50)}, + {Time(55.0), ScreenIntPoint(0, 55)}}, + Time(59.0), + ScreenIntPoint(0, 59)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(59.0), + ScreenIntPoint(0, 55)}, + + {Some(idEnd5), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, LimitFuturePrediction) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 44, then pause. UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(32.0), ScreenIntPoint(0, 32)}}, + Time(44.0), ScreenIntPoint(0, 44)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 44)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleMaxPredictMs == 8 + // Refuse to predict more than 8ms into the future, the fingers might have + // paused. Make an event for time 52 (= 44 + 8) instead of 59. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(32.0), ScreenIntPoint(0, 32)}, + {Time(44.0), ScreenIntPoint(0, 44)}}, + Time(52.0), + ScreenIntPoint(0, 52)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(52.0), + ScreenIntPoint(0, 44)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 44)}, + }); +} + +TEST_F(TouchResamplerTest, LimitBacksampling) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 44, then pause. UI thread is occupied until 64. + // Then we get a frame callback with a very outdated frametime. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(32.0), ScreenIntPoint(0, 32)}}, + Time(44.0), ScreenIntPoint(0, 44)); + resampler.NotifyFrame(Time(11.0)); // 16 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 44)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleMaxBacksampleMs == 20 + // Refuse to sample further back than 20ms before the last data point. + // Make an event for time 24 (= 44 - 20) instead of time 11. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}}, + Time(24.0), + ScreenIntPoint(0, 24)}, + + {Nothing(), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(32.0), ScreenIntPoint(0, 32)}}, + Time(44.0), + ScreenIntPoint(0, 44)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 44)}, + }); +} + +TEST_F(TouchResamplerTest, DontExtrapolateFromOldTouch) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 40, then pause. UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(30.0), ScreenIntPoint(0, 30)}}, + Time(40.0), ScreenIntPoint(0, 40)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 44)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleOldTouchThresholdMs == 17 + // Refuse to extrapolate from a data point that's more than 17ms older + // than the frame time. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(20.0), ScreenIntPoint(0, 20)}, + {Time(30.0), ScreenIntPoint(0, 30)}}, + Time(40.0), + ScreenIntPoint(0, 40)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 44)}, + }); +} + +TEST_F(TouchResamplerTest, DontExtrapolateIfTooOld) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 10, pause, and move again at 55. + // UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(55.0), + ScreenIntPoint(0, 55)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleWindowSize == 40 + // Refuse to resample between two data points that are more than 40ms + // apart. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(55.0), + ScreenIntPoint(0, 55)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, DontInterpolateIfTooOld) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + // Fingers move until time 10, pause, and move again at 60. + // UI thread is occupied until 64. + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(60.0), + ScreenIntPoint(0, 60)); + resampler.NotifyFrame(Time(59.0)); // 64 - 5 + auto idEnd2 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(70.0), + ScreenIntPoint(0, 60)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + // kTouchResampleWindowSize == 40 + // Refuse to resample between two data points that are more than 40ms + // apart. + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, + Time(60.0), + ScreenIntPoint(0, 60)}, + + {Some(idEnd2), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(70.0), + ScreenIntPoint(0, 60)}, + }); +} + +TEST_F(TouchResamplerTest, DiscardOutdatedHistoricalData) { + auto idStart0 = ProcessEvent(MultiTouchInput::MULTITOUCH_START, {}, Time(0.0), + ScreenIntPoint(0, 0)); + auto idMove1 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}}, Time(16.0), + ScreenIntPoint(0, 16)); + resampler.NotifyFrame(Time(20.0)); + auto idMove2 = ProcessEvent(MultiTouchInput::MULTITOUCH_MOVE, + {{Time(18.0), ScreenIntPoint(0, 18)}}, Time(25.0), + ScreenIntPoint(0, 25)); + auto idEnd3 = ProcessEvent(MultiTouchInput::MULTITOUCH_END, {}, Time(35.0), + ScreenIntPoint(0, 25)); + + CheckOutgoingEvents({ + {Some(idStart0), + MultiTouchInput::MULTITOUCH_START, + {}, + Time(0.0), + ScreenIntPoint(0, 0)}, + + {Some(idMove1), + MultiTouchInput::MULTITOUCH_MOVE, + {{Time(10.0), ScreenIntPoint(0, 10)}, + {Time(16.0), ScreenIntPoint(0, 16)}}, + Time(20.0), + ScreenIntPoint(0, 20)}, + + // Discard the historical data point from time 18, because we've already + // sent out an event with time 20 and don't want to go back before that. + {Some(idMove2), + MultiTouchInput::MULTITOUCH_MOVE, + {}, + Time(25.0), + ScreenIntPoint(0, 25)}, + + {Some(idEnd3), + MultiTouchInput::MULTITOUCH_END, + {}, + Time(35.0), + ScreenIntPoint(0, 25)}, + }); +} diff --git a/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp b/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp new file mode 100644 index 0000000000..5ac361fea0 --- /dev/null +++ b/widget/tests/gtest/TestWinHeaderOnlyUtils.cpp @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/WinHeaderOnlyUtils.h" + +#include <shlwapi.h> + +TEST(WinHeaderOnlyUtils, MozPathGetDriveNumber) +{ + constexpr auto TestPathGetDriveNumber = [](const wchar_t* aPath) { + return mozilla::MozPathGetDriveNumber(aPath) == + ::PathGetDriveNumberW(aPath); + }; + EXPECT_TRUE(TestPathGetDriveNumber(nullptr)); + EXPECT_TRUE(TestPathGetDriveNumber(L"")); + EXPECT_TRUE(TestPathGetDriveNumber(L" :")); + EXPECT_TRUE(TestPathGetDriveNumber(L"a:")); + EXPECT_TRUE(TestPathGetDriveNumber(L"C:\\file")); + EXPECT_TRUE(TestPathGetDriveNumber(L"x:/file")); + EXPECT_TRUE(TestPathGetDriveNumber(L"@:\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"B")); + EXPECT_TRUE(TestPathGetDriveNumber(L"abc:\\file")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\:A")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\c:\\")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\A")); + EXPECT_TRUE(TestPathGetDriveNumber(L"\\\\?\\ \\")); +} diff --git a/widget/tests/gtest/TestWinMessageLoggingUtils.cpp b/widget/tests/gtest/TestWinMessageLoggingUtils.cpp new file mode 100644 index 0000000000..b0ab09f866 --- /dev/null +++ b/widget/tests/gtest/TestWinMessageLoggingUtils.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "windows/nsWindowDbg.h"
+
+using namespace mozilla;
+using namespace mozilla::widget;
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_CombinationFlagsHandledFirst)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x3, "COMBO"}, {0x1, "ONE"}, {0x2, "TWO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x3, flags, nullptr));
+ EXPECT_STREQ("COMBO", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_SingleFlag)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x4, flags, nullptr));
+ EXPECT_STREQ("FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_SingleFlagWithName)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x4, flags, "paramName"));
+ EXPECT_STREQ("paramName=FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_MultipleFlags)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x5, flags, nullptr));
+ EXPECT_STREQ("ONE|FOUR", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NoFlags)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, nullptr));
+ EXPECT_STREQ("Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_OnlySomeFlagsMatch)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x9, flags, nullptr));
+ EXPECT_STREQ("ONE|Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_FlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x2, flags, nullptr));
+ EXPECT_STREQ("TWO", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NoFlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, nullptr));
+ EXPECT_STREQ("Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_NameAndNoFlagsMatch_NoZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x8, flags, "paramName"));
+ EXPECT_STREQ("paramName=Unknown (0x00000008)", result.get());
+}
+
+TEST(WinMessageLoggingUtils, AppendFlagsInfo_ValueIsZero_ZeroReturned)
+{
+ const nsTArray<EnumValueAndName> flags = {
+ {0x1, "ONE"}, {0x2, "TWO"}, {0x4, "FOUR"}, {0x0, "ZERO"}};
+ nsAutoCString result;
+ EXPECT_EQ(true, AppendFlagsInfo(result, 0x0, flags, nullptr));
+ EXPECT_STREQ("ZERO", result.get());
+}
diff --git a/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp b/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp new file mode 100644 index 0000000000..b1f1114a3a --- /dev/null +++ b/widget/tests/gtest/TestWinWindowOcclusionTracker.cpp @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include <dwmapi.h> +#include <windows.h> + +#include "MockWinWidget.h" +#include "mozilla/widget/WinWindowOcclusionTracker.h" +#include "mozilla/WindowsVersion.h" + +using namespace mozilla; +using namespace mozilla::widget; + +class WinWindowOcclusionTrackerTest : public ::testing::Test { + protected: + HWND CreateNativeWindow(DWORD aStyle, DWORD aExStyle) { + mMockWinWidget = + MockWinWidget::Create(WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | aStyle, + aExStyle, LayoutDeviceIntRect(0, 0, 100, 100)); + EXPECT_NE(nullptr, mMockWinWidget.get()); + HWND hwnd = mMockWinWidget->GetWnd(); + HRGN region = ::CreateRectRgn(0, 0, 0, 0); + EXPECT_NE(nullptr, region); + if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) { + // On Windows 7, the newly created window has a complex region, which + // means it will be ignored during the occlusion calculation. So, force + // it to have a simple region so that we get test coverage on win 7. + RECT boundingRect; + EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect)); + HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect); + EXPECT_NE(nullptr, rectangularRegion); + ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE); + ::DeleteObject(rectangularRegion); + } + ::DeleteObject(region); + + ::ShowWindow(hwnd, SW_SHOWNORMAL); + EXPECT_TRUE(UpdateWindow(hwnd)); + return hwnd; + } + + // Wrapper around IsWindowVisibleAndFullyOpaque so only the test class + // needs to be a friend of NativeWindowOcclusionTrackerWin. + bool CheckWindowVisibleAndFullyOpaque(HWND aHWnd, + LayoutDeviceIntRect* aWinRect) { + bool ret = WinWindowOcclusionTracker::IsWindowVisibleAndFullyOpaque( + aHWnd, aWinRect); + // In general, if IsWindowVisibleAndFullyOpaque returns false, the + // returned rect should not be altered. + if (!ret) { + EXPECT_EQ(*aWinRect, LayoutDeviceIntRect(0, 0, 0, 0)); + } + return ret; + } + + RefPtr<MockWinWidget> mMockWinWidget; +}; + +TEST_F(WinWindowOcclusionTrackerTest, VisibleOpaqueWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect returnedRect; + // Normal windows should be visible. + EXPECT_TRUE(CheckWindowVisibleAndFullyOpaque(hwnd, &returnedRect)); + + // Check that the returned rect == the actual window rect of the hwnd. + RECT winRect; + ASSERT_TRUE(::GetWindowRect(hwnd, &winRect)); + EXPECT_EQ(returnedRect, LayoutDeviceIntRect(winRect.left, winRect.top, + winRect.right - winRect.left, + winRect.bottom - winRect.top)); +} + +TEST_F(WinWindowOcclusionTrackerTest, MinimizedWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + ::ShowWindow(hwnd, SW_MINIMIZE); + // Minimized windows are not considered visible. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, TransparentWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_TRANSPARENT); + LayoutDeviceIntRect winRect; + // Transparent windows are not considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, ToolWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_TOOLWINDOW); + LayoutDeviceIntRect winRect; + // Tool windows are not considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, LayeredAlphaWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED); + LayoutDeviceIntRect winRect; + BYTE alpha = 1; + DWORD flags = LWA_ALPHA; + COLORREF colorRef = RGB(1, 1, 1); + SetLayeredWindowAttributes(hwnd, colorRef, alpha, flags); + // Layered windows with alpha < 255 are not considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, UpdatedLayeredAlphaWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED); + LayoutDeviceIntRect winRect; + HDC hdc = ::CreateCompatibleDC(nullptr); + EXPECT_NE(nullptr, hdc); + BLENDFUNCTION blend = {AC_SRC_OVER, 0x00, 0xFF, AC_SRC_ALPHA}; + + ::UpdateLayeredWindow(hwnd, hdc, nullptr, nullptr, nullptr, nullptr, + RGB(0xFF, 0xFF, 0xFF), &blend, ULW_OPAQUE); + // Layered windows set up with UpdateLayeredWindow instead of + // SetLayeredWindowAttributes should not be considered visible and opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); + ::DeleteDC(hdc); +} + +TEST_F(WinWindowOcclusionTrackerTest, LayeredNonAlphaWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, WS_EX_LAYERED); + LayoutDeviceIntRect winRect; + BYTE alpha = 1; + DWORD flags = 0; + COLORREF colorRef = RGB(1, 1, 1); + ::SetLayeredWindowAttributes(hwnd, colorRef, alpha, flags); + // Layered non alpha windows are considered visible and opaque. + EXPECT_TRUE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, ComplexRegionWindow) { + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + // Create a region with rounded corners, which should be a complex region. + HRGN region = CreateRoundRectRgn(1, 1, 100, 100, 5, 5); + EXPECT_NE(nullptr, region); + ::SetWindowRgn(hwnd, region, /* bRedraw = */ TRUE); + // Windows with complex regions are not considered visible and fully opaque. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); + DeleteObject(region); +} + +TEST_F(WinWindowOcclusionTrackerTest, PopupWindow) { + HWND hwnd = CreateNativeWindow(WS_POPUP, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + // Normal Popup Windows are not considered visible. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} + +TEST_F(WinWindowOcclusionTrackerTest, CloakedWindow) { + // Cloaking is only supported in Windows 8 and above. + if (!IsWin8OrLater()) { + return; + } + HWND hwnd = CreateNativeWindow(/* aStyle = */ 0, /* aExStyle = */ 0); + LayoutDeviceIntRect winRect; + BOOL cloak = TRUE; + ::DwmSetWindowAttribute(hwnd, DWMWA_CLOAK, &cloak, sizeof(cloak)); + // Cloaked Windows are not considered visible. + EXPECT_FALSE(CheckWindowVisibleAndFullyOpaque(hwnd, &winRect)); +} diff --git a/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp b/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp new file mode 100644 index 0000000000..13cd95651c --- /dev/null +++ b/widget/tests/gtest/TestWinWindowOcclusionTrackerInteractive.cpp @@ -0,0 +1,402 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include "MockWinWidget.h" +#include "mozilla/Preferences.h" +#include "mozilla/widget/WinEventObserver.h" +#include "mozilla/widget/WinWindowOcclusionTracker.h" +#include "nsThreadUtils.h" +#include "Units.h" +#include "WinUtils.h" + +using namespace mozilla; +using namespace mozilla::widget; + +#define PREF_DISPLAY_STATE \ + "widget.windows.window_occlusion_tracking_display_state.enabled" +#define PREF_SESSION_LOCK \ + "widget.windows.window_occlusion_tracking_session_lock.enabled" + +class WinWindowOcclusionTrackerInteractiveTest : public ::testing::Test { + protected: + void SetUp() override { + // Shut down WinWindowOcclusionTracker if it exists. + // This could happen when WinWindowOcclusionTracker was initialized by other + // gtest + if (WinWindowOcclusionTracker::Get()) { + WinWindowOcclusionTracker::ShutDown(); + } + EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get()); + + WinWindowOcclusionTracker::Ensure(); + EXPECT_NE(nullptr, WinWindowOcclusionTracker::Get()); + + WinWindowOcclusionTracker::Get()->EnsureDisplayStatusObserver(); + WinWindowOcclusionTracker::Get()->EnsureSessionChangeObserver(); + } + + void TearDown() override { + WinWindowOcclusionTracker::ShutDown(); + EXPECT_EQ(nullptr, WinWindowOcclusionTracker::Get()); + } + + void SetNativeWindowBounds(HWND aHWnd, const LayoutDeviceIntRect aBounds) { + RECT wr; + wr.left = aBounds.X(); + wr.top = aBounds.Y(); + wr.right = aBounds.XMost(); + wr.bottom = aBounds.YMost(); + + ::AdjustWindowRectEx(&wr, ::GetWindowLong(aHWnd, GWL_STYLE), FALSE, + ::GetWindowLong(aHWnd, GWL_EXSTYLE)); + + // Make sure to keep the window onscreen, as AdjustWindowRectEx() may have + // moved part of it offscreen. But, if the original requested bounds are + // offscreen, don't adjust the position. + LayoutDeviceIntRect windowBounds(wr.left, wr.top, wr.right - wr.left, + wr.bottom - wr.top); + + if (aBounds.X() >= 0) { + windowBounds.x = std::max(0, windowBounds.X()); + } + if (aBounds.Y() >= 0) { + windowBounds.y = std::max(0, windowBounds.Y()); + } + ::SetWindowPos(aHWnd, HWND_TOP, windowBounds.X(), windowBounds.Y(), + windowBounds.Width(), windowBounds.Height(), + SWP_NOREPOSITION); + EXPECT_TRUE(::UpdateWindow(aHWnd)); + } + + void CreateNativeWindowWithBounds(LayoutDeviceIntRect aBounds) { + mMockWinWidget = MockWinWidget::Create( + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle = */ 0, aBounds); + EXPECT_NE(nullptr, mMockWinWidget.get()); + HWND hwnd = mMockWinWidget->GetWnd(); + SetNativeWindowBounds(hwnd, aBounds); + HRGN region = ::CreateRectRgn(0, 0, 0, 0); + EXPECT_NE(nullptr, region); + if (::GetWindowRgn(hwnd, region) == COMPLEXREGION) { + // On Windows 7, the newly created window has a complex region, which + // means it will be ignored during the occlusion calculation. So, force + // it to have a simple region so that we get test coverage on win 7. + RECT boundingRect; + EXPECT_TRUE(::GetWindowRect(hwnd, &boundingRect)); + HRGN rectangularRegion = ::CreateRectRgnIndirect(&boundingRect); + EXPECT_NE(nullptr, rectangularRegion); + ::SetWindowRgn(hwnd, rectangularRegion, /* bRedraw = */ TRUE); + ::DeleteObject(rectangularRegion); + } + ::DeleteObject(region); + + ::ShowWindow(hwnd, SW_SHOWNORMAL); + EXPECT_TRUE(UpdateWindow(hwnd)); + } + + RefPtr<MockWinWidget> CreateTrackedWindowWithBounds( + LayoutDeviceIntRect aBounds) { + RefPtr<MockWinWidget> window = MockWinWidget::Create( + WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, /* aExStyle */ 0, aBounds); + EXPECT_NE(nullptr, window.get()); + HWND hwnd = window->GetWnd(); + ::ShowWindow(hwnd, SW_SHOWNORMAL); + WinWindowOcclusionTracker::Get()->Enable(window, window->GetWnd()); + return window; + } + + int GetNumVisibleRootWindows() { + return WinWindowOcclusionTracker::Get()->mNumVisibleRootWindows; + } + + void OnDisplayStateChanged(bool aDisplayOn) { + WinWindowOcclusionTracker::Get()->OnDisplayStateChanged(aDisplayOn); + } + + RefPtr<MockWinWidget> mMockWinWidget; +}; + +// Simple test completely covering a tracked window with a native window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::OCCLUDED); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test partially covering a tracked window with a native window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, PartialOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test that a partly off screen tracked window, with the on screen part +// occluded by a native window, is considered occluded. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, OffscreenOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + // Move the tracked window 50 pixels offscreen to the left. + int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + ::SetWindowPos(window->GetWnd(), HWND_TOP, screenLeft - 50, 0, 100, 100, + SWP_NOZORDER | SWP_NOSIZE); + + // Create a native window that covers the onscreen part of the tracked window. + CreateNativeWindowWithBounds(LayoutDeviceIntRect(screenLeft, 0, 50, 100)); + window->SetExpectation(widget::OcclusionState::OCCLUDED); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test with a tracked window and native window that do not overlap. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleVisible) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Simple test with a minimized tracked window and native window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, SimpleHidden) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(200, 0, 100, 100)); + // Iconify the tracked window and check that its occlusion state is HIDDEN. + ::CloseWindow(window->GetWnd()); + window->SetExpectation(widget::OcclusionState::HIDDEN); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that minimizing and restoring a tracked window results in the occlusion +// tracker re-registering for win events and detecting that a native window +// occludes the tracked window. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, + OcclusionAfterVisibilityToggle) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::HIDDEN); + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + window, /* aVisible = */ false); + + // This makes the window iconic. + ::CloseWindow(window->GetWnd()); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // HIDDEN state is set synchronously by OnWindowVsiblityChanged notification, + // before occlusion is calculated, so the above expectation will be met w/o an + // occlusion calculation. + // Loop until an occlusion calculation has run with no non-hidden windows. + while (GetNumVisibleRootWindows() != 0) { + // Need to pump events in order for UpdateOcclusionState to get called, and + // update the number of non hidden windows. When that number is 0, + // occlusion has been calculated with no visible tracked windows. + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::VISIBLE); + WinWindowOcclusionTracker::Get()->OnWindowVisibilityChanged( + window, /* aVisible = */ true); + // This opens the window made iconic above. + ::OpenIcon(window->GetWnd()); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // Open a native window that occludes the visible tracked window. + window->SetExpectation(widget::OcclusionState::OCCLUDED); + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that locking the screen causes visible windows to become occluded. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenVisibleOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::OCCLUDED); + // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker + // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but + // actually locking the screen isn't feasible. + DWORD currentSessionId = 0; + ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); + ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, + WTS_SESSION_LOCK, currentSessionId); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + MSG msg; + bool gotMessage = + ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); + PR_Sleep(PR_INTERVAL_NO_WAIT); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that locking the screen leaves hidden windows as hidden. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenHiddenOcclusion) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + // Iconify the tracked window and check that its occlusion state is HIDDEN. + ::CloseWindow(window->GetWnd()); + window->SetExpectation(widget::OcclusionState::HIDDEN); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // Force the state to VISIBLE. + window->NotifyOcclusionState(widget::OcclusionState::VISIBLE); + + window->SetExpectation(widget::OcclusionState::HIDDEN); + + // Unfortunately, this relies on knowing that NativeWindowOcclusionTracker + // uses SessionChangeObserver to listen for WM_WTSSESSION_CHANGE messages, but + // actually locking the screen isn't feasible. + DWORD currentSessionId = 0; + ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); + PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, + WTS_SESSION_LOCK, currentSessionId); + + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + MSG msg; + bool gotMessage = + ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); + PR_Sleep(PR_INTERVAL_NO_WAIT); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that locking the screen from a different session doesn't mark window +// as occluded. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, LockScreenDifferentSession) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 200, 200)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + // Force the state to OCCLUDED. + window->NotifyOcclusionState(widget::OcclusionState::OCCLUDED); + + // Generate a session change lock screen with a session id that's not + // currentSessionId. + DWORD currentSessionId = 0; + ::ProcessIdToSessionId(::GetCurrentProcessId(), ¤tSessionId); + ::PostMessage(WinEventHub::Get()->GetWnd(), WM_WTSSESSION_CHANGE, + WTS_SESSION_LOCK, currentSessionId + 1); + + window->SetExpectation(widget::OcclusionState::VISIBLE); + // Create a native window to trigger occlusion calculation. + CreateNativeWindowWithBounds(LayoutDeviceIntRect(0, 0, 50, 50)); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + MSG msg; + bool gotMessage = + ::PeekMessageW(&msg, WinEventHub::Get()->GetWnd(), 0, 0, PM_REMOVE); + if (gotMessage) { + ::TranslateMessage(&msg); + ::DispatchMessageW(&msg); + } + NS_ProcessNextEvent(nullptr, /* aMayWait = */ false); + PR_Sleep(PR_INTERVAL_NO_WAIT); + } + EXPECT_FALSE(window->IsExpectingCall()); +} + +// Test that display off & on power state notification causes visible windows to +// become occluded, then visible. +TEST_F(WinWindowOcclusionTrackerInteractiveTest, DisplayOnOffHandling) { + RefPtr<MockWinWidget> window = + CreateTrackedWindowWithBounds(LayoutDeviceIntRect(0, 0, 100, 100)); + window->SetExpectation(widget::OcclusionState::VISIBLE); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::OCCLUDED); + + // Turning display off and on isn't feasible, so send a notification. + OnDisplayStateChanged(/* aDisplayOn */ false); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + + window->SetExpectation(widget::OcclusionState::VISIBLE); + OnDisplayStateChanged(/* aDisplayOn */ true); + while (window->IsExpectingCall()) { + WinWindowOcclusionTracker::Get()->TriggerCalculation(); + + NS_ProcessNextEvent(nullptr, /* aMayWait = */ true); + } + EXPECT_FALSE(window->IsExpectingCall()); +} diff --git a/widget/tests/gtest/moz.build b/widget/tests/gtest/moz.build new file mode 100644 index 0000000000..613844fa78 --- /dev/null +++ b/widget/tests/gtest/moz.build @@ -0,0 +1,27 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES = [ + "TestTimeConverter.cpp", + "TestTouchResampler.cpp", +] + +if CONFIG["OS_ARCH"] == "WINNT": + UNIFIED_SOURCES += [ + "MockWinWidget.cpp", + "TestWinHeaderOnlyUtils.cpp", + "TestWinMessageLoggingUtils.cpp", + "TestWinWindowOcclusionTracker.cpp", + "TestWinWindowOcclusionTrackerInteractive.cpp", + ] + +FINAL_LIBRARY = "xul-gtest" + +LOCAL_INCLUDES += [ + "/widget", +] + +DisableStlWrapping() |