diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /widget/windows/tests/gtest | |
parent | Initial commit. (diff) | |
download | firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'widget/windows/tests/gtest')
-rw-r--r-- | widget/windows/tests/gtest/TestJumpListBuilder.cpp | 823 | ||||
-rw-r--r-- | widget/windows/tests/gtest/TestWinDND.cpp | 728 | ||||
-rw-r--r-- | widget/windows/tests/gtest/moz.build | 19 |
3 files changed, 1570 insertions, 0 deletions
diff --git a/widget/windows/tests/gtest/TestJumpListBuilder.cpp b/widget/windows/tests/gtest/TestJumpListBuilder.cpp new file mode 100644 index 0000000000..5494c42d37 --- /dev/null +++ b/widget/windows/tests/gtest/TestJumpListBuilder.cpp @@ -0,0 +1,823 @@ +/* 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 <objectarray.h> +#include <shobjidl.h> +#include <windows.h> +#include <string.h> +#include <propvarutil.h> +#include <propkey.h> + +#ifdef __MINGW32__ +// MinGW-w64 headers are missing PropVariantToString. +# include <propsys.h> +PSSTDAPI PropVariantToString(REFPROPVARIANT propvar, PWSTR psz, UINT cch); +#endif + +#include "gtest/gtest.h" +#include "gmock/gmock.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/WindowsJumpListShortcutDescriptionBinding.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "JumpListBuilder.h" + +using namespace mozilla; +using namespace testing; +using mozilla::dom::AutoJSAPI; +using mozilla::dom::Promise; +using mozilla::dom::PromiseNativeHandler; +using mozilla::dom::ToJSValue; +using mozilla::dom::WindowsJumpListShortcutDescription; +using mozilla::widget::JumpListBackend; +using mozilla::widget::JumpListBuilder; + +/** + * GMock matcher that ensures that two LPCWSTRs match. + */ +MATCHER_P(LPCWSTREq, value, "The equivalent of StrEq for LPCWSTRs") { + return (wcscmp(arg, value)) == 0; +} + +/** + * GMock matcher that ensures that a IObjectArray* contains nsIShellLinkW's + * that match an equivalent set of nsTArray<WindowsJumpListShortcutDescriptions> + */ +MATCHER_P(ShellLinksEq, descs, + "Comparing generated IShellLinkW with " + "WindowsJumpListShortcutDescription definitions") { + uint32_t count = 0; + HRESULT hr = arg->GetCount(&count); + if (FAILED(hr) || count != descs->Length()) { + return false; + } + + for (uint32_t i = 0; i < descs->Length(); ++i) { + RefPtr<IShellLinkW> link; + if (FAILED(arg->GetAt(i, IID_IShellLinkW, + static_cast<void**>(getter_AddRefs(link))))) { + return false; + } + + if (!link) { + return false; + } + + const WindowsJumpListShortcutDescription& desc = descs->ElementAt(i); + + // We'll now compare each member of the WindowsJumpListShortcutDescription + // with what is stored in the IShellLink. + + // WindowsJumpListShortcutDescription.title + IPropertyStore* propStore = nullptr; + hr = link->QueryInterface(IID_IPropertyStore, (LPVOID*)&propStore); + if (FAILED(hr)) { + return false; + } + + PROPVARIANT pv; + hr = propStore->GetValue(PKEY_Title, &pv); + if (FAILED(hr)) { + return false; + } + + wchar_t title[PKEYSTR_MAX]; + hr = PropVariantToString(pv, title, PKEYSTR_MAX); + if (FAILED(hr)) { + return false; + } + + if (!desc.mTitle.Equals(title)) { + return false; + } + + // WindowsJumpListShortcutDescription.path + wchar_t pathBuf[MAX_PATH]; + hr = link->GetPath(pathBuf, MAX_PATH, nullptr, SLGP_SHORTPATH); + if (FAILED(hr)) { + return false; + } + + if (!desc.mPath.Equals(pathBuf)) { + return false; + } + + // WindowsJumpListShortcutDescription.arguments (optional) + wchar_t argsBuf[MAX_PATH]; + hr = link->GetArguments(argsBuf, MAX_PATH); + if (FAILED(hr)) { + return false; + } + + if (desc.mArguments.WasPassed()) { + if (!desc.mArguments.Value().Equals(argsBuf)) { + return false; + } + } else { + // Otherwise, the arguments should be empty. + if (wcsnlen(argsBuf, MAX_PATH) != 0) { + return false; + } + } + + // WindowsJumpListShortcutDescription.description + wchar_t descBuf[MAX_PATH]; + hr = link->GetDescription(descBuf, MAX_PATH); + if (FAILED(hr)) { + return false; + } + + if (!desc.mDescription.Equals(descBuf)) { + return false; + } + + // WindowsJumpListShortcutDescription.iconPath and + // WindowsJumpListShortcutDescription.fallbackIconIndex + int iconIdx = 0; + wchar_t iconPathBuf[MAX_PATH]; + hr = link->GetIconLocation(iconPathBuf, MAX_PATH, &iconIdx); + if (FAILED(hr)) { + return false; + } + + if (desc.mIconPath.WasPassed() && !desc.mIconPath.Value().IsEmpty()) { + // If the WindowsJumpListShortcutDescription supplied an iconPath, + // then it should match iconPathBuf and have an icon index of 0. + if (!desc.mIconPath.Value().Equals(iconPathBuf) || iconIdx != 0) { + return false; + } + } else { + // Otherwise, the iconPathBuf should equal the + // WindowsJumpListShortcutDescription path, and the iconIdx should match + // the fallbackIconIndex. + if (!desc.mPath.Equals(iconPathBuf) || + desc.mFallbackIconIndex != iconIdx) { + return false; + } + } + } + + return true; +} + +/** + * This is a helper class that allows our tests to wait for a native DOM Promise + * to resolve, and get the JS::Value that the Promise resolves with. This is + * expected to run on the main thread. + */ +class WaitForResolver : public PromiseNativeHandler { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaitForResolver, override) + + NS_IMETHODIMP QueryInterface(REFNSIID aIID, void** aInstancePtr) override { + nsresult rv = NS_ERROR_UNEXPECTED; + NS_INTERFACE_TABLE0(WaitForResolver) + + return rv; + } + + WaitForResolver() : mIsDone(false) {} + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aError) override { + mResult = aValue; + mIsDone = true; + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aError) override { + ASSERT_TRUE(false); // Should never reach here. + } + + /** + * Spins a nested event loop and blocks until the Promise has resolved. + */ + void SpinUntilResolved() { + SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns, + [&]() { return mIsDone; }); + } + + /** + * Spins a nested event loop and blocks until the Promise has resolved, + * after which the JS::Value that the Promise resolves with is returned via + * the aRetval outparam. + * + * @param {JS::MutableHandle<JS::Value>} aRetval + * The outparam for the JS::Value that the Promise resolves with. + */ + void SpinUntilResolvedWithResult(JS::MutableHandle<JS::Value> aRetval) { + SpinEventLoopUntil("WaitForResolver::SpinUntilResolved"_ns, + [&]() { return mIsDone; }); + aRetval.set(mResult); + } + + private: + virtual ~WaitForResolver() = default; + + JS::Heap<JS::Value> mResult; + bool mIsDone; +}; + +/** + * An implementation of JumpListBackend that is instrumented using the GMock + * framework to record calls. Unlike the NativeJumpListBackend, this backend + * is expected to be instantiated on the main thread and passed as an argument + * to the JumpListBuilder's worker thread. Testers should wait for the methods + * that call these functions to resolve their Promises before checking the + * recorded values. + */ +class TestingJumpListBackend : public JumpListBackend { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JumpListBackend, override) + + TestingJumpListBackend() : mMonitor("TestingJumpListBackend::mMonitor") {} + + virtual bool IsAvailable() override { return true; } + + MOCK_METHOD(HRESULT, SetAppID, (LPCWSTR)); + MOCK_METHOD(HRESULT, BeginList, (UINT*, REFIID, void**)); + MOCK_METHOD(HRESULT, AddUserTasks, (IObjectArray*)); + MOCK_METHOD(HRESULT, AppendCategory, (LPCWSTR, IObjectArray*)); + MOCK_METHOD(HRESULT, CommitList, ()); + MOCK_METHOD(HRESULT, AbortList, ()); + MOCK_METHOD(HRESULT, DeleteList, (LPCWSTR)); + + virtual HRESULT AppendKnownCategory(KNOWNDESTCATEGORY category) override { + return 0; + } + + // In one case (construction), an operation occurs off of the main thread that + // we must wait for without an associated Promise. + Monitor& GetMonitor() { return mMonitor; } + + protected: + virtual ~TestingJumpListBackend() override{}; + + private: + Monitor mMonitor; +}; + +/** + * A helper function that creates some fake WindowsJumpListShortcutDescription + * objects as well as JS::Value representations of those objects. These are + * returned to the caller through outparams. + * + * @param {JSContext*} aCx + * The current JSContext in the execution environment. + * @param {uint32_t} howMany + * The number of WindowsJumpListShortcutDescriptions to generate. + * @param {boolean} longDescription + * True if the description should be greater than MAX_PATH (260 characters). + * @param {nsTArray<WindowsJumpListShortcutDescription>&} aArray + * The outparam for the array of generated + * WindowsJumpListShortcutDescriptions. + * @param {nsTArray<JS::Value>&} aJSValArray + * The outparam for the array of JS::Value's representing the generated + * WindowsJumpListShortcutDescriptions. + */ +void GenerateWindowsJumpListShortcutDescriptions( + JSContext* aCx, uint32_t howMany, bool longDescription, + nsTArray<WindowsJumpListShortcutDescription>& aArray, + nsTArray<JS::Value>& aJSValArray) { + for (uint32_t i = 0; i < howMany; ++i) { + WindowsJumpListShortcutDescription desc; + nsAutoString title(u"Test Task #"); + title.AppendInt(i); + desc.mTitle = title; + + nsAutoString path(u"C:\\Some\\Test\\Path.exe"); + desc.mPath = path; + nsAutoString description; + + if (longDescription) { + description.AppendPrintf( + "For item #%i, this is a very very very very VERY VERY very very " + "very very very very very very very very very very VERY VERY very " + "very very very very very very very very very very very VERY VERY " + "very very very very very very very very very very very very VERY " + "VERY very very very very very very very very very very very very " + "VERY VERY very very very very very very very very very very very " + "very VERY VERY very very very very very very very very long test " + "description for an item", + i); + } else { + description.AppendPrintf("This is a test description for an item #%i", i); + } + + desc.mDescription = description; + desc.mFallbackIconIndex = 0; + + if (!(i % 2)) { + nsAutoString arguments(u"-arg1 -arg2 -arg3"); + desc.mArguments.Construct(arguments); + nsAutoString iconPath(u"C:\\Some\\icon.png"); + desc.mIconPath.Construct(iconPath); + } + + aArray.AppendElement(desc); + JS::Rooted<JS::Value> descJSValue(aCx); + ASSERT_TRUE(ToJSValue(aCx, desc, &descJSValue)); + aJSValArray.AppendElement(std::move(descJSValue)); + } +} + +/** + * Tests construction and that the application ID is properly passed to the + * backend. + */ +TEST(JumpListBuilder, Construction) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + ASSERT_TRUE(testBackend); + + nsAutoString aumid(u"TestApplicationID"); + LPCWSTR passedID = aumid.get(); + // Construction of our class (or any class of that matter) does not return a + // Promise that we can wait on to ensure that the background thread got the + // right information. We therefore use a monitor on the testing backend as + // well as an EXPECT_CALL to block execution of the test until the background + // work has completed. + Monitor& mon = testBackend->GetMonitor(); + MonitorAutoLock lock(mon); + EXPECT_CALL(*testBackend, SetAppID(LPCWSTREq(passedID))).WillOnce([&mon] { + MonitorAutoLock lock(mon); + mon.Notify(); + return S_OK; + }); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + // This is the amount of time that we will wait for the background thread to + // respond before considering it a timeout failure. + const int kWaitTimeoutMs = 5000; + + ASSERT_TRUE(mon.Wait(TimeDuration::FromMilliseconds(kWaitTimeoutMs)) != + CVStatus::Timeout); +} + +/** + * Tests calling CheckForRemovals and receiving a series of removed jump list + * entries. Calling CheckForRemovals should call the following methods on the + * backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AbortList + */ +TEST(JumpListBuilder, CheckForRemovals) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + EXPECT_CALL(*testBackend, AbortList()).Times(2); + + // Let's prepare BeginList to return a two entry collection of IShellLinks. + // The first IShellLink will have the URL be https://example.com, the second + // will have the URL be https://mozilla.org. + EXPECT_CALL(*testBackend, BeginList) + .WillOnce([](UINT* pcMinSlots, REFIID riid, void** ppv) { + RefPtr<IObjectCollection> collection; + DebugOnly<HRESULT> hr = CoCreateInstance( + CLSID_EnumerableObjectCollection, nullptr, CLSCTX_INPROC_SERVER, + IID_IObjectCollection, getter_AddRefs(collection)); + MOZ_ASSERT(SUCCEEDED(hr)); + + RefPtr<IShellLinkW> link; + hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, + IID_IShellLinkW, getter_AddRefs(link)); + MOZ_ASSERT(SUCCEEDED(hr)); + + nsAutoString firstLinkHref(u"https://example.com"_ns); + link->SetArguments(firstLinkHref.get()); + + nsAutoString appPath(u"C:\\Tmp\\firefox.exe"_ns); + link->SetIconLocation(appPath.get(), 0); + + collection->AddObject(link); + + // Let's re-use the same IShellLink, but change the URL to add our + // second entry. The values of the IShellLink are ultimately copied + // over to the items being added to the collection. + nsAutoString secondLinkHref(u"https://mozilla.org"_ns); + link->SetArguments(secondLinkHref.get()); + collection->AddObject(link); + + RefPtr<IObjectArray> pArray; + hr = collection->QueryInterface(IID_IObjectArray, + getter_AddRefs(pArray)); + MOZ_ASSERT(SUCCEEDED(hr)); + + *ppv = static_cast<IObjectArray*>(pArray); + (static_cast<IUnknown*>(*ppv))->AddRef(); + + // This is the default value to return, according to + // https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist + *pcMinSlots = 10; + + return S_OK; + }); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + nsresult rv = builder->CheckForRemovals(cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolvedWithResult(&result); + + ASSERT_TRUE(result.isObject()); + JS::Rooted<JSObject*> obj(cx, result.toObjectOrNull()); + + bool isArray; + ASSERT_TRUE(JS::IsArrayObject(cx, obj, &isArray)); + ASSERT_TRUE(isArray); + + // We should expect to see 2 URL strings returned in the array. + uint32_t length = 0; + ASSERT_TRUE(JS::GetArrayLength(cx, obj, &length)); + ASSERT_EQ(length, 2U); + + // The first one should be https://example.com + JS::Rooted<JS::Value> firstURLValue(cx); + ASSERT_TRUE(JS_GetElement(cx, obj, 0, &firstURLValue)); + JS::Rooted<JSString*> firstURLJSString(cx, firstURLValue.toString()); + nsAutoJSString firstURLAutoString; + ASSERT_TRUE(firstURLAutoString.init(cx, firstURLJSString)); + + ASSERT_TRUE(firstURLAutoString.EqualsLiteral("https://example.com")); + + // The second one should be https://mozilla.org + JS::Rooted<JS::Value> secondURLValue(cx); + ASSERT_TRUE(JS_GetElement(cx, obj, 1, &secondURLValue)); + JS::Rooted<JSString*> secondURLJSString(cx, secondURLValue.toString()); + nsAutoJSString secondURLAutoString; + ASSERT_TRUE(secondURLAutoString.init(cx, secondURLJSString)); + + ASSERT_TRUE(secondURLAutoString.EqualsLiteral("https://mozilla.org")); +} + +/** + * Tests calling PopulateJumpList with empty arguments, which should call the + * following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - CommitList + * + * This should result in an empty jump list for the user. + */ +TEST(JumpListBuilder, PopulateJumpListEmpty) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<JS::Value> taskDescJSVals; + nsAutoString customTitle(u""); + nsTArray<JS::Value> customDescJSVals; + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling PopulateJumpList with only tasks, and no custom items. + * This should call the following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AddUserTasks + * - CommitList + * + * This should result in a jump list with just tasks shown to the user, and + * no custom section. + */ +TEST(JumpListBuilder, PopulateJumpListOnlyTasks) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<JS::Value> taskDescJSVals; + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs, + taskDescJSVals); + + nsAutoString customTitle(u""); + nsTArray<JS::Value> customDescJSVals; + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); + + EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling PopulateJumpList with only custom items, and no tasks. + * This should call the following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AppendCategory + * - CommitList + * + * This should result in a jump list with just custom items shown to the user, + * and no tasks. + */ +TEST(JumpListBuilder, PopulateJumpListOnlyCustomItems) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<WindowsJumpListShortcutDescription> descs; + nsTArray<JS::Value> customDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, descs, + customDescJSVals); + + nsAutoString customTitle(u"My custom title"); + nsTArray<JS::Value> taskDescJSVals; + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0); + + EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), + ShellLinksEq(&descs))) + .Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling PopulateJumpList with tasks and custom items. + * This should call the following methods on the backend, in order: + * + * - SetAppID + * - AbortList + * - BeginList + * - AddUserTasks + * - AppendCategory + * - CommitList + * + * This should result in a jump list with both built-in tasks as well as + * custom tasks with a custom label. + */ +TEST(JumpListBuilder, PopulateJumpList) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + nsTArray<JS::Value> taskDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, taskDescs, + taskDescJSVals); + + nsTArray<WindowsJumpListShortcutDescription> customDescs; + nsTArray<JS::Value> customDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, false, customDescs, + customDescJSVals); + + nsAutoString customTitle(u"My custom title"); + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); + + EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), + ShellLinksEq(&customDescs))) + .Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Tests calling ClearJumpList calls the following: + * + * - SetAppID + * - DeleteList (passing the aumid) + * + * This results in an empty jump list for the user. + */ +TEST(JumpListBuilder, ClearJumpList) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + EXPECT_CALL(*testBackend, AbortList()).Times(0); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(0); + EXPECT_CALL(*testBackend, AddUserTasks(_)).Times(0); + + EXPECT_CALL(*testBackend, AppendCategory(_, _)).Times(0); + EXPECT_CALL(*testBackend, CommitList()).Times(0); + EXPECT_CALL(*testBackend, DeleteList(LPCWSTREq(aumid.get()))).Times(1); + + nsresult rv = builder->ClearJumpList(cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} + +/** + * Test that a WindowsJumpListShortcutDescription with a description + * longer than MAX_PATH gets truncated to MAX_PATH. This is because a + * description longer than MAX_PATH will cause CommitList to fail. + */ +TEST(JumpListBuilder, TruncateDescription) +{ + RefPtr<StrictMock<TestingJumpListBackend>> testBackend = + new StrictMock<TestingJumpListBackend>(); + nsAutoString aumid(u"TestApplicationID"); + // We set up this expectation here because SetAppID will be called soon + // after construction of the JumpListBuilder via the background thread. + EXPECT_CALL(*testBackend, SetAppID(_)).Times(1); + + nsCOMPtr<nsIJumpListBuilder> builder = + new JumpListBuilder(aumid, testBackend); + ASSERT_TRUE(builder); + + AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JSContext* cx = jsapi.cx(); + RefPtr<Promise> promise; + + nsTArray<WindowsJumpListShortcutDescription> taskDescs; + nsTArray<JS::Value> taskDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, taskDescs, + taskDescJSVals); + + nsTArray<WindowsJumpListShortcutDescription> customDescs; + nsTArray<JS::Value> customDescJSVals; + GenerateWindowsJumpListShortcutDescriptions(cx, 2, true, customDescs, + customDescJSVals); + // We expect the long descriptions to be truncated to 260 characters, so + // we'll truncate the descriptions here ourselves. + for (auto& taskDesc : taskDescs) { + taskDesc.mDescription.SetLength(MAX_PATH - 1); + } + for (auto& customDesc : customDescs) { + customDesc.mDescription.SetLength(MAX_PATH - 1); + } + + nsAutoString customTitle(u"My custom title"); + + EXPECT_CALL(*testBackend, AbortList()).Times(1); + EXPECT_CALL(*testBackend, BeginList(_, _, _)).Times(1); + EXPECT_CALL(*testBackend, AddUserTasks(ShellLinksEq(&taskDescs))).Times(1); + + EXPECT_CALL(*testBackend, AppendCategory(LPCWSTREq(customTitle.get()), + ShellLinksEq(&customDescs))) + .Times(1); + EXPECT_CALL(*testBackend, CommitList()).Times(1); + EXPECT_CALL(*testBackend, DeleteList(_)).Times(0); + + nsresult rv = + builder->PopulateJumpList(taskDescJSVals, customTitle, customDescJSVals, + cx, getter_AddRefs(promise)); + ASSERT_TRUE(NS_SUCCEEDED(rv)); + ASSERT_TRUE(promise); + + RefPtr<WaitForResolver> resolver = new WaitForResolver(); + promise->AppendNativeHandler(resolver); + JS::Rooted<JS::Value> result(cx); + resolver->SpinUntilResolved(); +} diff --git a/widget/windows/tests/gtest/TestWinDND.cpp b/widget/windows/tests/gtest/TestWinDND.cpp new file mode 100644 index 0000000000..fb7849fd79 --- /dev/null +++ b/widget/windows/tests/gtest/TestWinDND.cpp @@ -0,0 +1,728 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <windows.h> +#include <ole2.h> +#include <shlobj.h> + +#include "nsArray.h" +#include "nsArrayUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" + +#include "nsClipboard.h" +#include "nsDataObjCollection.h" + +#include "gtest/gtest.h" + +// shims for conversion from cppunittest to gtest +template <size_t N> +void fail(const char (&msg)[N]) { + ADD_FAILURE() << "TEST-UNEXPECTED-FAIL | " << msg; +} +template <size_t N> +void passed(const char (&msg)[N]) { + GTEST_SUCCEED() << "TEST-PASS | " << msg; +} + +nsIFile* xferFile; + +nsresult CheckValidHDROP(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + DROPFILES* pDropFiles; + pDropFiles = (DROPFILES*)GlobalLock(hGlobal); + if (!pDropFiles) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + if (pDropFiles->pFiles != sizeof(DROPFILES)) { + fail("DROPFILES struct has wrong size"); + } + + if (!pDropFiles->fWide) { + fail("Received data is not Unicode"); + return NS_ERROR_UNEXPECTED; + } + nsString s; + unsigned long offset = 0; + while (true) { + s = (char16_t*)((char*)pDropFiles + pDropFiles->pFiles + offset); + if (s.IsEmpty()) break; + nsresult rv; + nsCOMPtr<nsIFile> localFile( + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv)); + rv = localFile->InitWithPath(s); + if (NS_FAILED(rv)) { + fail("File could not be opened"); + return NS_ERROR_UNEXPECTED; + } + offset += sizeof(char16_t) * (s.Length() + 1); + } + return NS_OK; +} + +nsresult CheckValidTEXT(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char* pText; + pText = (char*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsCString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult CheckValidTEXTTwo(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char* pText; + pText = (char*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsCString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult CheckValidUNICODE(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char16_t* pText; + pText = (char16_t*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult CheckValidUNICODETwo(STGMEDIUM* pSTG) { + if (pSTG->tymed != TYMED_HGLOBAL) { + fail("Received data is not in an HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + HGLOBAL hGlobal = pSTG->hGlobal; + char16_t* pText; + pText = (char16_t*)GlobalLock(hGlobal); + if (!pText) { + fail("There is no data at the given HGLOBAL"); + return NS_ERROR_UNEXPECTED; + } + + nsString string; + string = pText; + + if (!string.EqualsLiteral("Mozilla can drag and drop twice over")) { + fail("Text passed through drop object wrong"); + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult GetTransferableFile(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferFile); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("application/x-moz-file", genericWrapper); + return rv; +} + +nsresult GetTransferableText(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + constexpr auto mozString = u"Mozilla can drag and drop"_ns; + nsCOMPtr<nsISupportsString> xferString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + rv = xferString->SetData(mozString); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("text/plain", genericWrapper); + return rv; +} + +nsresult GetTransferableTextTwo(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + constexpr auto mozString = u" twice over"_ns; + nsCOMPtr<nsISupportsString> xferString = + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + rv = xferString->SetData(mozString); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferString); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("text/plain", genericWrapper); + return rv; +} + +nsresult GetTransferableURI(nsCOMPtr<nsITransferable>& pTransferable) { + nsresult rv; + + nsCOMPtr<nsIURI> xferURI; + + rv = NS_NewURI(getter_AddRefs(xferURI), "http://www.mozilla.org"); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> genericWrapper = do_QueryInterface(xferURI); + + pTransferable = do_CreateInstance("@mozilla.org/widget/transferable;1"); + pTransferable->Init(nullptr); + rv = pTransferable->SetTransferData("text/x-moz-url", genericWrapper); + return rv; +} + +nsresult MakeDataObject(nsIArray* transferableArray, + RefPtr<IDataObject>& itemToDrag) { + nsresult rv; + uint32_t itemCount = 0; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), "http://www.mozilla.org"); + NS_ENSURE_SUCCESS(rv, rv); + + rv = transferableArray->GetLength(&itemCount); + NS_ENSURE_SUCCESS(rv, rv); + + // Copied more or less exactly from nsDragService::InvokeDragSession + // This is what lets us play fake Drag Service for the test + if (itemCount > 1) { + nsDataObjCollection* dataObjCollection = new nsDataObjCollection(); + if (!dataObjCollection) return NS_ERROR_OUT_OF_MEMORY; + itemToDrag = dataObjCollection; + for (uint32_t i = 0; i < itemCount; ++i) { + nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, i); + if (trans) { + RefPtr<IDataObject> dataObj; + rv = nsClipboard::CreateNativeDataObject(trans, getter_AddRefs(dataObj), + uri); + NS_ENSURE_SUCCESS(rv, rv); + // Add the flavors to the collection object too + rv = nsClipboard::SetupNativeDataObject(trans, dataObjCollection); + NS_ENSURE_SUCCESS(rv, rv); + + dataObjCollection->AddDataObject(dataObj); + } + } + } // if dragging multiple items + else { + nsCOMPtr<nsITransferable> trans = do_QueryElementAt(transferableArray, 0); + if (trans) { + rv = nsClipboard::CreateNativeDataObject(trans, + getter_AddRefs(itemToDrag), uri); + NS_ENSURE_SUCCESS(rv, rv); + } + } // else dragging a single object + return rv; +} + +nsresult Do_CheckOneFile() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableFile(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("File data object does not support the file data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("File data object did not provide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidHDROP(stg); + if (NS_FAILED(rv)) { + fail("HDROP was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + return NS_OK; +} + +nsresult Do_CheckTwoFiles() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableFile(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = GetTransferableFile(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_HDROP, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("File data object does not support the file data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("File data object did not provide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidHDROP(stg); + if (NS_FAILED(rv)) { + fail("HDROP was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + return NS_OK; +} + +nsresult Do_CheckOneString() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the ASCII text data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide ASCII data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidTEXT(stg); + if (NS_FAILED(rv)) { + fail("TEXT was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the wide text data type!"); + return NS_ERROR_UNEXPECTED; + } + + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide wide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidUNICODE(stg); + if (NS_FAILED(rv)) { + fail("UNICODE was invalid"); + return rv; + } + + return NS_OK; +} + +nsresult Do_CheckTwoStrings() { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = GetTransferableTextTwo(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + FORMATETC fe; + SET_FORMATETC(fe, CF_TEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the ASCII text data type!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg; + stg = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide ASCII data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidTEXTTwo(stg); + if (NS_FAILED(rv)) { + fail("TEXT was invalid"); + return rv; + } + + ReleaseStgMedium(stg); + + SET_FORMATETC(fe, CF_UNICODETEXT, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("String data object does not support the wide text data type!"); + return NS_ERROR_UNEXPECTED; + } + + if (dataObj->GetData(&fe, stg) != S_OK) { + fail("String data object did not provide wide data on request"); + return NS_ERROR_UNEXPECTED; + } + + rv = CheckValidUNICODETwo(stg); + if (NS_FAILED(rv)) { + fail("UNICODE was invalid"); + return rv; + } + + return NS_OK; +} + +nsresult Do_CheckSetArbitraryData(bool aMultiple) { + nsresult rv; + nsCOMPtr<nsITransferable> transferable; + nsCOMPtr<nsIMutableArray> transferableArray = nsArray::Create(); + nsCOMPtr<nsISupports> genericWrapper; + RefPtr<IDataObject> dataObj; + + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + + if (aMultiple) { + rv = GetTransferableText(transferable); + if (NS_FAILED(rv)) { + fail("Could not create the proper nsITransferable!"); + return rv; + } + genericWrapper = do_QueryInterface(transferable); + rv = transferableArray->AppendElement(genericWrapper); + if (NS_FAILED(rv)) { + fail("Could not append element to transferable array"); + return rv; + } + } + + rv = MakeDataObject(transferableArray, dataObj); + if (NS_FAILED(rv)) { + fail("Could not create data object"); + return rv; + } + + static CLIPFORMAT mozArbitraryFormat = + ::RegisterClipboardFormatW(L"MozillaTestFormat"); + FORMATETC fe; + STGMEDIUM stg; + SET_FORMATETC(fe, mozArbitraryFormat, 0, DVASPECT_CONTENT, -1, TYMED_HGLOBAL); + + HGLOBAL hg = GlobalAlloc(GPTR, 1024); + stg.tymed = TYMED_HGLOBAL; + stg.hGlobal = hg; + stg.pUnkForRelease = nullptr; + + if (dataObj->SetData(&fe, &stg, true) != S_OK) { + if (aMultiple) { + fail("Unable to set arbitrary data type on data object collection!"); + } else { + fail("Unable to set arbitrary data type on data object!"); + } + return NS_ERROR_UNEXPECTED; + } + + if (dataObj->QueryGetData(&fe) != S_OK) { + fail("Arbitrary data set on data object is not advertised!"); + return NS_ERROR_UNEXPECTED; + } + + STGMEDIUM* stg2; + stg2 = (STGMEDIUM*)CoTaskMemAlloc(sizeof(STGMEDIUM)); + if (dataObj->GetData(&fe, stg2) != S_OK) { + fail("Data object did not provide arbitrary data upon request!"); + return NS_ERROR_UNEXPECTED; + } + + if (stg2->hGlobal != hg) { + fail("Arbitrary data was not returned properly!"); + return rv; + } + ReleaseStgMedium(stg2); + + return NS_OK; +} + +// This function performs basic drop tests, testing a data object consisting +// of one transferable +nsresult Do_Test1() { + nsresult rv = NS_OK; + nsresult workingrv; + + workingrv = Do_CheckOneFile(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on a single file"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working file drag object!"); + } + + workingrv = Do_CheckOneString(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on a single string"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working string drag object!"); + } + + workingrv = Do_CheckSetArbitraryData(false); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on setting arbitrary data"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully set arbitrary data on a drag object"); + } + + return rv; +} + +// This function performs basic drop tests, testing a data object consisting of +// two transferables. +nsresult Do_Test2() { + nsresult rv = NS_OK; + nsresult workingrv; + + workingrv = Do_CheckTwoFiles(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on multiple files"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working multiple file drag object!"); + } + + workingrv = Do_CheckTwoStrings(); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on multiple strings"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully created a working multiple string drag object!"); + } + + workingrv = Do_CheckSetArbitraryData(true); + if (NS_FAILED(workingrv)) { + fail("Drag object tests failed on setting arbitrary data"); + rv = NS_ERROR_UNEXPECTED; + } else { + passed("Successfully set arbitrary data on a drag object"); + } + + return rv; +} + +// This function performs advanced drag and drop tests, testing a data object +// consisting of multiple transferables that have different data types +nsresult Do_Test3() { + nsresult rv = NS_OK; + // nsresult workingrv; + + // XXX TODO Write more advanced tests in Bug 535860 + return rv; +} + +nsCOMPtr<nsIFile> GetTemporaryDirectory() { + nsCOMPtr<nsIFile> tmpdir; + +#define ENSURE(expr) NS_ENSURE_SUCCESS(expr, nullptr); + + ENSURE(NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpdir))); + MOZ_ASSERT(tmpdir); + + ENSURE(tmpdir->AppendNative("TestWinDND"_ns)); + ENSURE(tmpdir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0777)); + +#undef ENSURE + + return tmpdir; +} + +TEST(TestWinDND, All) +{ + nsCOMPtr<nsIFile> file = GetTemporaryDirectory(); + if (!file) { + fail("could not create temporary directory!"); + return; + } + xferFile = file; + + if (NS_SUCCEEDED(Do_Test1())) { + passed( + "Basic Drag and Drop data type tests (single transferable) succeeded!"); + } + + if (NS_SUCCEEDED(Do_Test2())) { + passed( + "Basic Drag and Drop data type tests (multiple transferables) " + "succeeded!"); + } + + // if (NS_SUCCEEDED(Do_Test3())) + // passed("Advanced Drag and Drop data type tests succeeded!"); +} diff --git a/widget/windows/tests/gtest/moz.build b/widget/windows/tests/gtest/moz.build new file mode 100644 index 0000000000..4057b7ae57 --- /dev/null +++ b/widget/windows/tests/gtest/moz.build @@ -0,0 +1,19 @@ +# -*- 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 += [ + "TestJumpListBuilder.cpp", + "TestWinDND.cpp", +] + +LOCAL_INCLUDES += [ + "/widget", + "/widget/windows", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" |