diff --git a/change/react-native-windows-2020-04-09-16-51-25-wsmodule_unittests.json b/change/react-native-windows-2020-04-09-16-51-25-wsmodule_unittests.json new file mode 100644 index 00000000000..c5e9d9c9e0d --- /dev/null +++ b/change/react-native-windows-2020-04-09-16-51-25-wsmodule_unittests.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "packageName": "react-native-windows", + "email": "julio@rochsquadron.net", + "dependentChangeType": "none", + "date": "2020-04-09T23:51:25.594Z" +} \ No newline at end of file diff --git a/change/react-native-windows-2020-04-09-16-51-56-wsmodule_unittests.json b/change/react-native-windows-2020-04-09-16-51-56-wsmodule_unittests.json new file mode 100644 index 00000000000..a09e3a1389d --- /dev/null +++ b/change/react-native-windows-2020-04-09-16-51-56-wsmodule_unittests.json @@ -0,0 +1,8 @@ +{ + "type": "none", + "comment": "Allow WebSocketModule to use custom resource factory", + "packageName": "react-native-windows", + "email": "julio@rochsquadron.net", + "dependentChangeType": "none", + "date": "2020-04-09T23:51:56.846Z" +} \ No newline at end of file diff --git a/vnext/Desktop.UnitTests/InstanceMocks.cpp b/vnext/Desktop.UnitTests/InstanceMocks.cpp new file mode 100644 index 00000000000..1eb48c50fb1 --- /dev/null +++ b/vnext/Desktop.UnitTests/InstanceMocks.cpp @@ -0,0 +1,98 @@ +#include "InstanceMocks.h" + +#include + +using namespace facebook::react; + +using folly::dynamic; +using std::function; +using std::make_shared; +using std::make_unique; +using std::shared_ptr; +using std::string; +using std::unique_ptr; +using std::vector; + +namespace Microsoft::React::Test { +#pragma region MockMessageQueueThread +void MockMessageQueueThread::runOnQueue(function &&work) /*override*/ +{ + work(); +} + +void MockMessageQueueThread::runOnQueueSync(function &&work) /*override*/ +{ + work(); +} + +void MockMessageQueueThread::quitSynchronous() /*override*/ {} +#pragma endregion // MockMessageQueueThread + +#pragma region MockInstanceCallback + +void MockInstanceCallback::onBatchComplete() /*override*/ {} +void MockInstanceCallback::incrementPendingJSCalls() /*override*/ {} +void MockInstanceCallback::decrementPendingJSCalls() /*override*/ {} + +#pragma endregion // MockInstanceCallback + +#pragma region MockJSExecutor + +void MockJSExecutor::loadApplicationScript(unique_ptr script, string sourceURL) {} +void MockJSExecutor::setBundleRegistry(unique_ptr bundleRegistry) {} +void MockJSExecutor::registerBundle(uint32_t bundleId, const string &bundlePath) {} + +void MockJSExecutor::callFunction(const string &moduleId, const string &methodId, const dynamic &arguments) { + if (CallFunctionMock) { + CallFunctionMock(moduleId, methodId, arguments); + } +} + +void MockJSExecutor::invokeCallback(const double callbackId, const dynamic &arguments) {} +void MockJSExecutor::setGlobalVariable(string propName, unique_ptr jsonValue) {} +void *MockJSExecutor::getJavaScriptContext() { + return nullptr; +} + +bool MockJSExecutor::isInspectable() { + return false; +} +std::string MockJSExecutor::getDescription() { + return {}; +} +void MockJSExecutor::handleMemoryPressure(__unused int pressureLevel) {} +void MockJSExecutor::destroy() {} +void MockJSExecutor::flush() {} + +#pragma endregion // MockJSExecutor + +#pragma region MockJSExecutorFactory + +unique_ptr MockJSExecutorFactory::createJSExecutor( + shared_ptr delegate, + shared_ptr jsQueue) /*override*/ +{ + if (CreateJSExecutorMock) { + return CreateJSExecutorMock(delegate, jsQueue); + } + + return make_unique(); +} + +#pragma endregion // MockJSExecutorFactory + +shared_ptr CreateMockInstance(shared_ptr jsef) { + auto instance = make_shared(); + auto callback = make_unique(); + auto jsQueue = make_shared(); + auto moduleRegistry = make_shared( + vector>(), // modules + nullptr // moduleNotFoundCallback + ); + + instance->initializeBridge(std::move(callback), jsef, jsQueue, moduleRegistry); + + return instance; +} + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.UnitTests/InstanceMocks.h b/vnext/Desktop.UnitTests/InstanceMocks.h new file mode 100644 index 00000000000..bb535c79ece --- /dev/null +++ b/vnext/Desktop.UnitTests/InstanceMocks.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Microsoft::React::Test { +std::shared_ptr CreateMockInstance(std::shared_ptr jsef); + +class MockMessageQueueThread : public facebook::react::MessageQueueThread { + public: +#pragma region MessageQueueThread overrides + + void runOnQueue(std::function &&) override; + + void runOnQueueSync(std::function &&) override; + + void quitSynchronous() override; + +#pragma endregion // MessageQueueThread overrides +}; + +class MockInstanceCallback : public facebook::react::InstanceCallback { + public: +#pragma region InstanceCallback overrides + + void onBatchComplete() override; + void incrementPendingJSCalls() override; + void decrementPendingJSCalls() override; + +#pragma endregion // InstanceCallback overrides +}; + +class MockJSExecutor : public facebook::react::JSExecutor { + public: + std::function CallFunctionMock; + +#pragma region JSExecutor overrides + + void loadApplicationScript(std::unique_ptr script, std::string sourceURL) + override; + + void setBundleRegistry(std::unique_ptr bundleRegistry) override; + + void registerBundle(uint32_t bundleId, const std::string &bundlePath) override; + + void callFunction(const std::string &moduleId, const std::string &methodId, const folly::dynamic &arguments) override; + + void invokeCallback(const double callbackId, const folly::dynamic &arguments) override; + + void setGlobalVariable(std::string propName, std::unique_ptr jsonValue) override; + + void *getJavaScriptContext() override; + + bool isInspectable() override; + + std::string getDescription() override; + + void handleMemoryPressure(__unused int pressureLevel) override; + + void destroy() override; + + void flush() override; + +#pragma endregion // JSExecutor overrides +}; + +class MockJSExecutorFactory : public facebook::react::JSExecutorFactory { + public: + std::function( + std::shared_ptr, + std::shared_ptr)> + CreateJSExecutorMock; + +#pragma region JSExecutorFactory overrides + + std::unique_ptr createJSExecutor( + std::shared_ptr delegate, + std::shared_ptr jsQueue) override; + +#pragma endregion // JSExecutorFactory overrides +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj index 25d21585994..548a182d84d 100644 --- a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj +++ b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj @@ -87,12 +87,14 @@ + + @@ -104,7 +106,9 @@ + + diff --git a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters index 7ef3ff227b3..b473de81f8b 100644 --- a/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters +++ b/vnext/Desktop.UnitTests/React.Windows.Desktop.UnitTests.vcxproj.filters @@ -46,6 +46,12 @@ Source Files + + Source Files + + + Source Files + @@ -68,5 +74,11 @@ Header Files + + Header Files + + + Header Files + \ No newline at end of file diff --git a/vnext/Desktop.UnitTests/WebSocketMocks.cpp b/vnext/Desktop.UnitTests/WebSocketMocks.cpp new file mode 100644 index 00000000000..a57365cff9d --- /dev/null +++ b/vnext/Desktop.UnitTests/WebSocketMocks.cpp @@ -0,0 +1,43 @@ +#include "WebSocketMocks.h" + +using std::function; +using std::string; + +namespace Microsoft::React::Test { +#pragma region IWebSocketResource overrides + +void MockWebSocketResource::Connect(const Protocols &, const Options &) /*override*/ +{ + m_onConnect(); +} + +void MockWebSocketResource::Ping() /*override*/ {} + +void MockWebSocketResource::Send(const string &) /*override*/ {} + +void MockWebSocketResource::SendBinary(const string &) /*override*/ {} + +void MockWebSocketResource::Close(CloseCode, const string &) /*override*/ {} + +IWebSocketResource::ReadyState MockWebSocketResource::GetReadyState() const /*override*/ +{ + return ReadyState::Open; +} + +void MockWebSocketResource::SetOnConnect(function &&onConnect) /*override*/ +{ + m_onConnect = std::move(onConnect); +} + +void MockWebSocketResource::SetOnPing(function &&) /*override*/ {} + +void MockWebSocketResource::SetOnSend(function &&) /*override*/ {} + +void MockWebSocketResource::SetOnMessage(function &&) /*override*/ {} + +void MockWebSocketResource::SetOnClose(function &&) /*override*/ {} + +void MockWebSocketResource::SetOnError(function &&) /*override*/ {} + +#pragma endregion IWebSocketResource overrides +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.UnitTests/WebSocketMocks.h b/vnext/Desktop.UnitTests/WebSocketMocks.h new file mode 100644 index 00000000000..beeceac830b --- /dev/null +++ b/vnext/Desktop.UnitTests/WebSocketMocks.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace Microsoft::React::Test { +struct MockWebSocketResource : public IWebSocketResource { +#pragma region IWebSocketResource overrides + + void Connect(const Protocols &, const Options &) override; + + void Ping() override; + + void Send(const std::string &) override; + + void SendBinary(const std::string &) override; + + void Close(CloseCode, const std::string &) override; + + ReadyState GetReadyState() const override; + + void SetOnConnect(std::function &&onConnect) override; + + void SetOnPing(std::function &&) override; + + void SetOnSend(std::function &&) override; + + void SetOnMessage(std::function &&) override; + + void SetOnClose(std::function &&) override; + + void SetOnError(std::function &&) override; + +#pragma endregion IWebSocketResource overrides + + private: + std::function m_onConnect; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp b/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp index cced3d4ab59..0aa7d57dba3 100644 --- a/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp +++ b/vnext/Desktop.UnitTests/WebSocketModuleTest.cpp @@ -3,31 +3,76 @@ #include #include +#include "InstanceMocks.h" +#include "WebSocketMocks.h" using namespace facebook::react; using namespace facebook::xplat::module; using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using folly::dynamic; +using std::function; +using std::make_shared; +using std::make_unique; +using std::shared_ptr; +using std::string; +using std::unique_ptr; +using std::vector; + namespace Microsoft::React::Test { TEST_CLASS (WebSocketModuleTest) { - enum class MethodId : size_t { Connect = 0, Close = 1, Send = 2, SendBinary = 3, Ping = 4, SIZE = 5 }; - - const char *MethodName[static_cast(MethodId::SIZE)]{"connect", "close", "send", "sendBinary", "ping"}; + const char *MethodName[static_cast(WebSocketModule::MethodId::SIZE)]{ + "connect", "close", "send", "sendBinary", "ping"}; - TEST_METHOD(WebSocketModuleTest_CreateModule) { - auto module = std::make_unique(); + TEST_METHOD(CreateModule) { + auto module = make_unique(); Assert::IsFalse(module == nullptr); - Assert::AreEqual(std::string("WebSocketModule"), module->getName()); + Assert::AreEqual(string("WebSocketModule"), module->getName()); auto methods = module->getMethods(); - for (size_t i = 0; i < static_cast(MethodId::SIZE); i++) { - Assert::AreEqual(std::string(MethodName[i]), std::string(methods[i].name)); + for (size_t i = 0; i < static_cast(WebSocketModule::MethodId::SIZE); i++) { + Assert::AreEqual(string(MethodName[i]), string(methods[i].name)); } Assert::AreEqual(static_cast(0), module->getConstants().size()); } + + TEST_METHOD(ConnectSendsEvent) { + string eventName; + string moduleName; + string methodName; + auto jsef = make_shared(); + jsef->CreateJSExecutorMock = [&eventName, &moduleName, &methodName]( + shared_ptr, shared_ptr) { + auto jse = make_unique(); + jse->CallFunctionMock = [&eventName, &moduleName, &methodName]( + const string &module, const string &method, const dynamic &args) { + moduleName = module; + methodName = method; + eventName = args.at(0).asString(); + }; + + return std::move(jse); + }; + + auto instance = CreateMockInstance(jsef); + auto module = make_unique(); + module->setInstance(instance); + module->SetResourceFactory([](const string &, bool, bool) { return make_shared(); }); + + // Execute module method + auto connect = module->getMethods().at(WebSocketModule::MethodId::Connect); + connect.func( + dynamic::array("ws://localhost:0", dynamic(), dynamic(), /*id*/ 0), + [](vector) {}, + [](vector) {}); + + Assert::AreEqual({"RCTDeviceEventEmitter"}, moduleName); + Assert::AreEqual({"emit"}, methodName); + Assert::AreEqual({"websocketOpen"}, eventName); + } }; } // namespace Microsoft::React::Test diff --git a/vnext/Desktop/Modules/WebSocketModule.cpp b/vnext/Desktop/Modules/WebSocketModule.cpp index ef20c0baa7e..6ac369f22f0 100644 --- a/vnext/Desktop/Modules/WebSocketModule.cpp +++ b/vnext/Desktop/Modules/WebSocketModule.cpp @@ -15,6 +15,7 @@ using namespace folly; using Microsoft::Common::Unicode::Utf8ToUtf16; +using std::shared_ptr; using std::string; using std::weak_ptr; @@ -24,7 +25,15 @@ constexpr char moduleName[] = "WebSocketModule"; namespace Microsoft::React { -WebSocketModule::WebSocketModule() {} +WebSocketModule::WebSocketModule() + : m_resourceFactory{[](const string &url, bool legacyImplementation, bool acceptSelfSigned) { + return IWebSocketResource::Make(url, legacyImplementation, acceptSelfSigned); + }} {} + +void WebSocketModule::SetResourceFactory( + std::function(const string &, bool, bool)> &&resourceFactory) { + m_resourceFactory = std::move(resourceFactory); +} string WebSocketModule::getName() { return moduleName; @@ -146,7 +155,7 @@ std::shared_ptr WebSocketModule::GetOrCreateWebSocket(int64_ auto itr = m_webSockets.find(id); if (itr == m_webSockets.end()) { - auto ws = IWebSocketResource::Make(std::move(url)); + auto ws = m_resourceFactory(std::move(url), /*legacyImplementation*/ false, /*acceptSelfSigned*/ false); auto weakInstance = this->getInstance(); ws->SetOnError([this, id, weakInstance](const IWebSocketResource::Error& err) { diff --git a/vnext/ReactWindowsCore/Modules/WebSocketModule.h b/vnext/ReactWindowsCore/Modules/WebSocketModule.h index bc2e4ca3fef..2b4dbaf5a86 100644 --- a/vnext/ReactWindowsCore/Modules/WebSocketModule.h +++ b/vnext/ReactWindowsCore/Modules/WebSocketModule.h @@ -18,21 +18,28 @@ class WebSocketModule : public facebook::xplat::module::CxxModule { WebSocketModule(); +#pragma region CxxModule overrides + /// /// /// - std::string getName(); + std::string getName() override; /// /// /// - virtual std::map getConstants(); + std::map getConstants() override; /// /// /// /// See See react-native/Libraries/WebSocket/WebSocket.js - virtual std::vector getMethods(); + std::vector getMethods() override; + +#pragma endregion CxxModule overrides + + void SetResourceFactory( + std::function(const std::string &, bool, bool)> &&resourceFactory = nullptr); private: /// @@ -50,6 +57,11 @@ class WebSocketModule : public facebook::xplat::module::CxxModule { /// As defined in WebSocket.js. /// std::map> m_webSockets; + + /// + /// Generates IWebSocketResource instances, defaulting to IWebSocketResource::Make. + /// + std::function(const std::string &, bool, bool)> m_resourceFactory; }; } // namespace Microsoft::React