diff --git a/.ado/build-template.yml b/.ado/build-template.yml index 69a0c6f6556..43a3cb1dccd 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -265,31 +265,20 @@ extends: timeoutInMinutes: 360 # CodeQL requires 3x usual build timeout variables: - template: .ado/variables/windows.yml@self - # Enable if any issues RNTesterIntegrationTests::* become unstable. + # Enable if any issues RNTesterHeadlessTests::* become unstable. - name: Desktop.IntegrationTests.SkipRNTester value: false - #5059 - Disable failing or intermittent tests (IntegrationTestHarness,WebSocket,Logging). #10732 - WebSocketIntegrationTest::SendReceiveSsl fails on Windows Server 2022. #12714 - Disable for first deployment of test website. - #14217 - Reneable RNTesterIntegrationTests - name: Desktop.IntegrationTests.Filter value: > - (FullyQualifiedName!=RNTesterIntegrationTests::IntegrationTestHarness)& - (FullyQualifiedName!=RNTesterIntegrationTests::WebSocket)& - (FullyQualifiedName!=RNTesterIntegrationTests::WebSocketBlob)& - (FullyQualifiedName!=RNTesterIntegrationTests::WebSocketMultipleSend)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::ConnectClose)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::ConnectNoClose)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveClose)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendConsecutive)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveLargeMessage)& (FullyQualifiedName!=Microsoft::React::Test::WebSocketIntegrationTest::SendReceiveSsl)& - (FullyQualifiedName!=Microsoft::React::Test::HttpOriginPolicyIntegrationTest)& - (FullyQualifiedName!=RNTesterIntegrationTests::Dummy)& - (FullyQualifiedName!=RNTesterIntegrationTests::Fetch)& - (FullyQualifiedName!=RNTesterIntegrationTests::XHRSample)& - (FullyQualifiedName!=RNTesterIntegrationTests::Blob)& - (FullyQualifiedName!=RNTesterIntegrationTests::Logging) + (FullyQualifiedName!=Microsoft::React::Test::HttpOriginPolicyIntegrationTest) #6799 - HostFunctionTest, HostObjectProtoTest crash under JSI/V8; # PreparedJavaScriptSourceTest asserts/fails under JSI/ChakraCore - name: Desktop.UnitTests.Filter diff --git a/.ado/jobs/desktop-single.yml b/.ado/jobs/desktop-single.yml index 38c41c33fd3..c3cafc12aa8 100644 --- a/.ado/jobs/desktop-single.yml +++ b/.ado/jobs/desktop-single.yml @@ -21,27 +21,17 @@ parameters: - Continuous steps: + - template: ../templates/checkout-shallow.yml + # Set up IIS for integration tests - pwsh: | Install-WindowsFeature -Name Web-Server, Web-Scripting-Tools displayName: Install IIS - pwsh: | - function Invoke-WebRequestWithRetry($Uri, $OutFile, $MaxRetries = 3) { - for ($i = 1; $i -le $MaxRetries; $i++) { - try { - Write-Host "Downloading $OutFile (attempt $i of $MaxRetries)" - Invoke-WebRequest -Uri $Uri -OutFile $OutFile - return - } catch { - Write-Host "Attempt $i failed: $_" - if ($i -eq $MaxRetries) { throw } - Start-Sleep -Seconds (5 * $i) - } - } - } + $DownloadScript = "$(Build.SourcesDirectory)\vnext\Scripts\Tfs\Invoke-WebRequestWithRetry.ps1" - Invoke-WebRequestWithRetry ` + & $DownloadScript ` -Uri 'https://download.visualstudio.microsoft.com/download/pr/20598243-c38f-4538-b2aa-af33bc232f80/ea9b2ca232f59a6fdc84b7a31da88464/dotnet-hosting-8.0.3-win.exe' ` -OutFile dotnet-hosting-8.0.3-win.exe @@ -49,7 +39,7 @@ steps: Start-Process -Wait -FilePath .\dotnet-hosting-8.0.3-win.exe -ArgumentList '/INSTALL', '/QUIET', '/NORESTART' Write-Host 'Installed .NET hosting bundle' - Invoke-WebRequestWithRetry ` + & $DownloadScript ` -Uri 'https://download.visualstudio.microsoft.com/download/pr/f2ec926e-0d98-4a8b-8c70-722ccc2ca0e5/b59941b0c60f16421679baafdb7e9338/dotnet-sdk-7.0.407-win-x64.exe' ` -OutFile dotnet-sdk-7.0.407-win-x64.exe @@ -58,7 +48,17 @@ steps: Write-Host 'Installed .NET 7 SDK' displayName: Install the .NET Core Hosting Bundle - - template: ../templates/checkout-shallow.yml + - pwsh: | + $DownloadScript = "$(Build.SourcesDirectory)\vnext\Scripts\Tfs\Invoke-WebRequestWithRetry.ps1" + + & $DownloadScript ` + -Uri 'https://aka.ms/windowsappsdk/1.8/latest/windowsappruntimeinstall-x64.exe' ` + -OutFile windowsappruntimeinstall-x64.exe + + Write-Host 'Installing Windows App SDK Runtime' + Start-Process -Wait -FilePath .\windowsappruntimeinstall-x64.exe -ArgumentList '--quiet' + Write-Host 'Installed Windows App SDK Runtime' + displayName: Install Windows App SDK Runtime - template: ../templates/prepare-js-env.yml @@ -72,7 +72,7 @@ steps: - ${{ if eq(variables['Desktop.IntegrationTests.SkipRNTester'], true) }}: - pwsh: | - $newValue = '(FullyQualifiedName!~RNTesterIntegrationTests::)&' + "$(Desktop.IntegrationTests.Filter)" + $newValue = '(FullyQualifiedName!~Microsoft::React::Test::RNTesterHeadlessTests::)&' + "$(Desktop.IntegrationTests.Filter)" Write-Host "##vso[task.setvariable variable=Desktop.IntegrationTests.Filter]$newValue" displayName: Update Desktop.IntegrationTests.Filter to exclude RNTester integration tests diff --git a/Directory.Build.targets b/Directory.Build.targets index a8b078a0544..806c6058f3d 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -108,6 +108,8 @@ RnwNewArch; UseFabric; FollyDir; + IncludeFabricInterface; + UseFabric; YogaDir; WinVer; " /> diff --git a/change/react-native-windows-e83b01b1-0133-47da-8a8a-c88a6c16398b.json b/change/react-native-windows-e83b01b1-0133-47da-8a8a-c88a6c16398b.json new file mode 100644 index 00000000000..2126bd891be --- /dev/null +++ b/change/react-native-windows-e83b01b1-0133-47da-8a8a-c88a6c16398b.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Re-introduce Desktop integration tests", + "packageName": "react-native-windows", + "email": "julio.rocha@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/@rnw-scripts/unbroken/bin.js b/packages/@rnw-scripts/unbroken/bin.js old mode 100644 new mode 100755 diff --git a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj index 2cc3f9e5700..e3689a7623c 100644 --- a/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj +++ b/vnext/Desktop.DLL/React.Windows.Desktop.DLL.vcxproj @@ -179,7 +179,7 @@ - + diff --git a/vnext/Desktop.IntegrationTests/Modules/TestAppState.cpp b/vnext/Desktop.IntegrationTests/Modules/TestAppState.cpp new file mode 100644 index 00000000000..57626154120 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/Modules/TestAppState.cpp @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestAppState.h" + +namespace Microsoft::React::Test { + +void AppState::Initialize(winrt::Microsoft::ReactNative::ReactContext const &) noexcept {} + +void AppState::GetCurrentAppState( + std::function const &success, + std::function const &) noexcept { + success({.app_state = "active"}); +} + +void AppState::AddListener(std::string) noexcept {} + +void AppState::RemoveListeners(double) noexcept {} + +::Microsoft::ReactNativeSpecs::AppStateSpec_AppStateConstants AppState::GetConstants() noexcept { + return {.initialAppState = "active"}; +} + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/Modules/TestAppState.h b/vnext/Desktop.IntegrationTests/Modules/TestAppState.h new file mode 100644 index 00000000000..394179f54ac --- /dev/null +++ b/vnext/Desktop.IntegrationTests/Modules/TestAppState.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +namespace Microsoft::React::Test { + +REACT_MODULE(AppState) +struct AppState { + using ModuleSpec = ::Microsoft::ReactNativeSpecs::AppStateSpec; + + REACT_INIT(Initialize) + void Initialize(winrt::Microsoft::ReactNative::ReactContext const &) noexcept; + + REACT_METHOD(GetCurrentAppState, L"getCurrentAppState") + void GetCurrentAppState( + std::function const &success, + std::function const &) noexcept; + + REACT_METHOD(AddListener, L"addListener") + void AddListener(std::string) noexcept; + + REACT_METHOD(RemoveListeners, L"removeListeners") + void RemoveListeners(double) noexcept; + + REACT_GET_CONSTANTS(GetConstants) + ::Microsoft::ReactNativeSpecs::AppStateSpec_AppStateConstants GetConstants() noexcept; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/Modules/TestDeviceInfo.cpp b/vnext/Desktop.IntegrationTests/Modules/TestDeviceInfo.cpp new file mode 100644 index 00000000000..59b8a201c65 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/Modules/TestDeviceInfo.cpp @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestDeviceInfo.h" + +namespace Microsoft::React::Test { + +void DeviceInfo::Initialize(winrt::Microsoft::ReactNative::ReactContext const &) noexcept {} + +::Microsoft::ReactNativeSpecs::DeviceInfoSpec_DeviceInfoConstants DeviceInfo::GetConstants() noexcept { + ::Microsoft::ReactNativeSpecs::DeviceInfoSpec_DeviceInfoConstants constants; + ::Microsoft::ReactNativeSpecs::DeviceInfoSpec_DisplayMetrics dm; + dm.fontScale = 1; + dm.height = 1024; + dm.width = 1024; + dm.scale = 1; + constants.Dimensions.screen = dm; + constants.Dimensions.window = dm; + return constants; +} + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/Modules/TestDeviceInfo.h b/vnext/Desktop.IntegrationTests/Modules/TestDeviceInfo.h new file mode 100644 index 00000000000..7dfaf40df9e --- /dev/null +++ b/vnext/Desktop.IntegrationTests/Modules/TestDeviceInfo.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +namespace Microsoft::React::Test { + +REACT_MODULE(DeviceInfo) +struct DeviceInfo { + using ModuleSpec = ::Microsoft::ReactNativeSpecs::DeviceInfoSpec; + + REACT_INIT(Initialize) + void Initialize(winrt::Microsoft::ReactNative::ReactContext const &) noexcept; + + REACT_GET_CONSTANTS(GetConstants) + ::Microsoft::ReactNativeSpecs::DeviceInfoSpec_DeviceInfoConstants GetConstants() noexcept; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/Modules/TestModule.h b/vnext/Desktop.IntegrationTests/Modules/TestModule.h new file mode 100644 index 00000000000..40a2fa5b93e --- /dev/null +++ b/vnext/Desktop.IntegrationTests/Modules/TestModule.h @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +#include + +// Standard Library +#include +#include +#include + +namespace Microsoft::React::Test { + +enum class TestStatus { Pending = 0, Passed, Failed }; + +REACT_MODULE(TestModule) +struct TestModule { + // Static test signaling - call Reset() before each test. + static void Reset() noexcept { + ResetEvent(s_completed.get()); + s_status = TestStatus::Pending; + } + + static TestStatus AwaitCompletion(DWORD timeoutMs = INFINITE) noexcept { + WaitForSingleObject(s_completed.get(), timeoutMs); + return s_status; + } + + REACT_INIT(Initialize) + void Initialize(winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept { + m_reactContext = reactContext; + } + + REACT_METHOD(MarkTestCompleted, L"markTestCompleted") + void MarkTestCompleted() noexcept { + MarkTestPassed(true); + } + + REACT_METHOD(MarkTestPassed, L"markTestPassed") + void MarkTestPassed(bool success) noexcept { + s_status = success ? TestStatus::Passed : TestStatus::Failed; + SetEvent(s_completed.get()); + } + + REACT_METHOD(VerifySnapshot, L"verifySnapshot") + void VerifySnapshot(std::function const &callback) noexcept { + // Snapshot testing is not supported on Windows; always report success. + callback(true); + } + + REACT_METHOD(SendAppEvent, L"sendAppEvent") + void SendAppEvent(std::string name, ::React::JSValue body) noexcept { + m_reactContext.EmitJSEvent(L"RCTDeviceEventEmitter", winrt::to_hstring(name), body); + } + + REACT_METHOD(ShouldResolve, L"shouldResolve") + void ShouldResolve(::React::ReactPromise &&result) noexcept { + result.Resolve(1); + } + + REACT_METHOD(ShouldReject, L"shouldReject") + void ShouldReject(::React::ReactPromise<::React::JSValue> &&result) noexcept { + result.Reject(::React::ReactError{}); + } + + TestStatus Status() const noexcept { + return s_status; + } + + private: + winrt::Microsoft::ReactNative::ReactContext m_reactContext; + + static inline std::atomic s_status{TestStatus::Pending}; + static inline winrt::handle s_completed{CreateEvent(nullptr, TRUE /*manualReset*/, FALSE /*initialState*/, nullptr)}; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/RNTesterHeadlessTests.cpp b/vnext/Desktop.IntegrationTests/RNTesterHeadlessTests.cpp new file mode 100644 index 00000000000..f25eb3cdd4d --- /dev/null +++ b/vnext/Desktop.IntegrationTests/RNTesterHeadlessTests.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include + +#include + +#include +#include + +#include "Modules/TestModule.h" +#include "TestReactNativeHostHolder.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace msrn = winrt::Microsoft::ReactNative; + +namespace Microsoft::React::Test { + +TEST_CLASS (RNTesterHeadlessTests) { + TEST_CLASS_INITIALIZE(Initialize) { + // https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/win32/mddbootstrap/nf-mddbootstrap-mddbootstrapinitialize2 + winrt::uninit_apartment(); + winrt::init_apartment(winrt::apartment_type::multi_threaded); + + if (FAILED(MddBootstrapInitialize2( + Microsoft::WindowsAppSDK::Release::MajorMinor, + Microsoft::WindowsAppSDK::Release::VersionTag, + {Microsoft::WindowsAppSDK::Runtime::Version::UInt64}, + MddBootstrapInitializeOptions::MddBootstrapInitializeOptions_None))) { + throw std::exception("Could not initialize Windows App runtime"); + } + } + + TEST_CLASS_CLEANUP(Cleanup) { + MddBootstrapShutdown(); + } + + TEST_METHOD(Dummy) { + TestModule::Reset(); + + winrt::handle instanceLoadedEvent{CreateEvent(nullptr, TRUE, FALSE, nullptr)}; + bool instanceFailed{false}; + + auto holder = TestReactNativeHostHolder( + L"IntegrationTests/DummyTest", + [&instanceLoadedEvent, &instanceFailed](msrn::ReactNativeHost const &host) noexcept { + host.InstanceSettings().InstanceLoaded( + [&instanceLoadedEvent, &instanceFailed](auto const &, msrn::InstanceLoadedEventArgs args) noexcept { + instanceFailed = args.Failed(); + SetEvent(instanceLoadedEvent.get()); + }); + }); + + // First, wait for instance to load + WaitForSingleObject(instanceLoadedEvent.get(), INFINITE); + if (instanceFailed) { + auto err = holder.GetLastError(); + auto msg = L"InstanceLoaded reported failure: " + (err.empty() ? L"(no error captured)" : err); + Assert::Fail(msg.c_str()); + } + + auto status = TestModule::AwaitCompletion(); + Assert::IsTrue(status == TestStatus::Passed, L"Test did not pass (JS did not call markTestPassed within timeout)"); + } +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj index 36ba1f247a7..69d10e47a67 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj @@ -79,7 +79,6 @@ FOLLY_NO_CONFIG; NOMINMAX; _HAS_AUTO_PTR_ETC; - RN_PLATFORM=win32; RN_EXPORT=; JSI_EXPORT=; %(PreprocessorDefinitions) @@ -88,10 +87,13 @@ $(VCInstallDir)UnitTest\include; "$(ReactNativeWindowsDir)Microsoft.ReactNative"; + "$(ReactNativeWindowsDir)Microsoft.ReactNative\ReactHost"; "$(ReactNativeWindowsDir)\Shared\tracing"; + "$(IntDir)\..\React.Windows.Desktop\Generated Files"; %(AdditionalIncludeDirectories) true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) @@ -114,8 +116,15 @@ + + + + + + + @@ -136,15 +145,27 @@ + + + + + + + + + + + + diff --git a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters index 39ab943e9e0..45c06fc788b 100644 --- a/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters +++ b/vnext/Desktop.IntegrationTests/React.Windows.Desktop.IntegrationTests.vcxproj.filters @@ -30,15 +30,36 @@ Source Files\Modules + + Source Files\Modules + + + Source Files\Modules + Integration Tests + + Integration Tests + Source Files Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + @@ -47,11 +68,32 @@ Header Files\Modules + + Header Files\Modules + + + Header Files\Modules + Header Files + + Header Files + Header Files + + Header Files + + + Header Files + + + Header Files + + + Header Files\Modules + \ No newline at end of file diff --git a/vnext/Desktop.IntegrationTests/TestCompositionContext.h b/vnext/Desktop.IntegrationTests/TestCompositionContext.h new file mode 100644 index 00000000000..134b1a61c81 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestCompositionContext.h @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// Test implementations of the Composition.Experimental interfaces so that +// Fabric can run in headless (ui-less) integration tests. Every method is a +// no-op and every property returns a sensible default. + +namespace Microsoft::React::Test { + +namespace Exp = winrt::Microsoft::ReactNative::Composition::Experimental; + +// ---------- IBrush / IDrawingSurfaceBrush ---------- + +struct TestBrush : winrt::implements {}; + +struct TestDrawingSurfaceBrush : winrt::implements { + void HorizontalAlignmentRatio(float) {} + void VerticalAlignmentRatio(float) {} + void Stretch(Exp::CompositionStretch) {} +}; + +// ---------- IDropShadow ---------- + +struct TestDropShadow : winrt::implements { + void Offset(winrt::Windows::Foundation::Numerics::float3) {} + void Opacity(float) {} + void BlurRadius(float) {} + void Color(winrt::Windows::UI::Color) {} + void Mask(Exp::IBrush) {} + void SourcePolicy(Exp::CompositionDropShadowSourcePolicy) {} +}; + +// ---------- IVisual (shared base for visual mocks) ---------- + +template +struct TestVisualBase : winrt::implements { + void InsertAt(Exp::IVisual, int32_t) {} + void Remove(Exp::IVisual) {} + Exp::IVisual GetAt(uint32_t) { + return nullptr; + } + void Opacity(float) {} + void Scale(winrt::Windows::Foundation::Numerics::float3) {} + void TransformMatrix(winrt::Windows::Foundation::Numerics::float4x4) {} + void RotationAngle(float) {} + void IsVisible(bool) {} + void Size(winrt::Windows::Foundation::Numerics::float2) {} + void Offset(winrt::Windows::Foundation::Numerics::float3) {} + void Offset(winrt::Windows::Foundation::Numerics::float3, winrt::Windows::Foundation::Numerics::float3) {} + void RelativeSizeWithOffset( + winrt::Windows::Foundation::Numerics::float2, + winrt::Windows::Foundation::Numerics::float2) {} + Exp::BackfaceVisibility BackfaceVisibility() { + return Exp::BackfaceVisibility::Visible; + } + void BackfaceVisibility(Exp::BackfaceVisibility) {} + winrt::hstring Comment() { + return {}; + } + void Comment(winrt::hstring const &) {} + void AnimationClass(Exp::AnimationClass) {} +}; + +// ---------- ISpriteVisual ---------- + +struct TestSpriteVisual : TestVisualBase { + void Brush(Exp::IBrush) {} + void Shadow(Exp::IDropShadow) {} +}; + +// ---------- IRoundedRectangleVisual ---------- + +struct TestRoundedRectangleVisual + : TestVisualBase { + void Brush(Exp::IBrush) {} + void CornerRadius(winrt::Windows::Foundation::Numerics::float2) {} + void StrokeBrush(Exp::IBrush) {} + void StrokeThickness(float) {} +}; + +// ---------- IScrollVisual ---------- + +struct TestScrollVisual : TestVisualBase { + void Brush(Exp::IBrush) {} + void ScrollEnabled(bool) {} + winrt::event_token ScrollPositionChanged( + winrt::Windows::Foundation::EventHandler const &) { + return {}; + } + void ScrollPositionChanged(winrt::event_token) {} + winrt::event_token ScrollBeginDrag( + winrt::Windows::Foundation::EventHandler const &) { + return {}; + } + void ScrollBeginDrag(winrt::event_token) {} + winrt::event_token ScrollEndDrag(winrt::Windows::Foundation::EventHandler const &) { + return {}; + } + void ScrollEndDrag(winrt::event_token) {} + winrt::event_token ScrollMomentumBegin( + winrt::Windows::Foundation::EventHandler const &) { + return {}; + } + void ScrollMomentumBegin(winrt::event_token) {} + winrt::event_token ScrollMomentumEnd( + winrt::Windows::Foundation::EventHandler const &) { + return {}; + } + void ScrollMomentumEnd(winrt::event_token) {} + void ContentSize(winrt::Windows::Foundation::Numerics::float2) {} + winrt::Windows::Foundation::Numerics::float3 ScrollPosition() { + return {}; + } + void ScrollBy(winrt::Windows::Foundation::Numerics::float3, bool) {} + void TryUpdatePosition(winrt::Windows::Foundation::Numerics::float3, bool) {} + void OnPointerPressed(winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs const &) {} + void SetDecelerationRate(winrt::Windows::Foundation::Numerics::float3) {} + void SetMaximumZoomScale(float) {} + void SetMinimumZoomScale(float) {} + bool Horizontal() { + return false; + } + void Horizontal(bool) {} + void SetSnapPoints(bool, bool, winrt::Windows::Foundation::Collections::IVectorView const &) {} + void PagingEnabled(bool) {} + void SnapToInterval(float) {} + void SnapToAlignment(Exp::SnapPointsAlignment) {} +}; + +// ---------- IActivityVisual ---------- + +struct TestActivityVisual : TestVisualBase { + using TestVisualBase::Size; // IVisual::Size(float2) + void Size(float) {} // IActivityVisual::Size(float) + void Brush(Exp::IBrush) {} + void StartAnimation() {} + void StopAnimation() {} +}; + +// ---------- ICaretVisual ---------- + +struct TestCaretVisual : winrt::implements { + Exp::IVisual InnerVisual() { + return winrt::make(); + } + void Size(winrt::Windows::Foundation::Numerics::float2) {} + void Position(winrt::Windows::Foundation::Numerics::float2) {} + bool IsVisible() { + return false; + } + void IsVisible(bool) {} + void Brush(Exp::IBrush) {} +}; + +// ---------- IFocusVisual ---------- + +struct TestFocusVisual : winrt::implements { + Exp::IVisual InnerVisual() { + return winrt::make(); + } + bool IsFocused() { + return false; + } + void IsFocused(bool) {} + float ScaleFactor() { + return 1.0f; + } + void ScaleFactor(float) {} +}; + +// ---------- ICompositionContext ---------- + +struct TestCompositionContext : winrt::implements { + Exp::ISpriteVisual CreateSpriteVisual() { + return winrt::make(); + } + Exp::IScrollVisual CreateScrollerVisual() { + return winrt::make(); + } + Exp::IRoundedRectangleVisual CreateRoundedRectangleVisual() { + return winrt::make(); + } + Exp::IActivityVisual CreateActivityVisual() { + return winrt::make(); + } + Exp::ICaretVisual CreateCaretVisual() { + return winrt::make(); + } + Exp::IFocusVisual CreateFocusVisual() { + return winrt::make(); + } + Exp::IDropShadow CreateDropShadow() { + return winrt::make(); + } + Exp::IBrush CreateColorBrush(winrt::Windows::UI::Color) { + return winrt::make(); + } + Exp::IDrawingSurfaceBrush CreateDrawingSurfaceBrush( + winrt::Windows::Foundation::Size, + winrt::Windows::Graphics::DirectX::DirectXPixelFormat, + winrt::Windows::Graphics::DirectX::DirectXAlphaMode) { + return winrt::make(); + } +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/TestReactNativeHostHolder.cpp b/vnext/Desktop.IntegrationTests/TestReactNativeHostHolder.cpp new file mode 100644 index 00000000000..b530ef645cf --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestReactNativeHostHolder.cpp @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestReactNativeHostHolder.h" + +#include "TestReactPackageProvider.h" +#include "TestUIDispatcher.h" + +#include +#include +#include +#include + +#include "TestCompositionContext.h" + +namespace Microsoft::React::Test { + +msrn::ReactPropertyId PlatformNameOverrideProperty() noexcept { + static msrn::ReactPropertyId prop{L"ReactNative.Injection", L"PlatformNameOverride"}; + return prop; +} + +TestReactNativeHostHolder::TestReactNativeHostHolder( + std::wstring_view jsBundle, + std::function hostInitializer, + Options &&options) noexcept { + m_host = winrt::Microsoft::ReactNative::ReactNativeHost{}; + m_queueController = winrt::Microsoft::UI::Dispatching::DispatcherQueueController::CreateOnDedicatedThread(); + m_uiDispatcher = winrt::make(m_queueController.DispatcherQueue()); + + m_queueController.DispatcherQueue().TryEnqueue([this, + jsBundle = std::wstring{jsBundle}, + hostInitializer = std::move(hostInitializer), + options = std::move(options)]() noexcept { + auto settings = m_host.InstanceSettings(); + settings.JavaScriptBundleFile(jsBundle); + settings.Properties().Set(msrn::ReactDispatcherHelper::UIDispatcherProperty(), m_uiDispatcher); + settings.Properties().Set(PlatformNameOverrideProperty().Handle(), winrt::box_value(L"windows")); + settings.UseDeveloperSupport(false); + settings.UseFastRefresh(true); + settings.UseLiveReload(false); + settings.EnableDeveloperMenu(false); + settings.PackageProviders().Append(winrt::make()); + + // Capture errors for diagnostics + settings.NativeLogger([this](msrn::LogLevel level, winrt::hstring const &message) { + if (static_cast(level) >= static_cast(msrn::LogLevel::Error)) { + std::lock_guard lock(m_errorMutex); + if (m_lastError.empty()) { + m_lastError = message; + } + } + }); + + // Enable Fabric by setting a stub CompositionContext. + // In a headless test we have no real compositor, but this is sufficient + // for the runtime to choose the Fabric code path. + winrt::Microsoft::ReactNative::ReactPropertyBag(settings.Properties()) + .Set( + winrt::Microsoft::ReactNative::ReactPropertyId< + winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext>{ + L"ReactNative.Composition", L"CompositionContext"}, + winrt::make()); + + hostInitializer(m_host); + + if (options.LoadInstance) { + m_host.LoadInstance(); + } + }); +} + +TestReactNativeHostHolder::~TestReactNativeHostHolder() noexcept { + m_host.UnloadInstance().get(); + m_queueController.ShutdownQueueAsync().get(); +} + +winrt::Microsoft::ReactNative::ReactNativeHost const &TestReactNativeHostHolder::Host() const noexcept { + return m_host; +} + +std::wstring TestReactNativeHostHolder::GetLastError() const noexcept { + std::lock_guard lock(m_errorMutex); + return m_lastError; +} + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/TestReactNativeHostHolder.h b/vnext/Desktop.IntegrationTests/TestReactNativeHostHolder.h new file mode 100644 index 00000000000..d248339251e --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestReactNativeHostHolder.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace Microsoft::React::Test { + +namespace msrn = winrt::Microsoft::ReactNative; + +struct TestReactNativeHostHolder { + struct Options { + bool LoadInstance = true; + }; + + TestReactNativeHostHolder( + std::wstring_view jsBundle, + std::function hostInitializer, + Options &&options = {}) noexcept; + ~TestReactNativeHostHolder() noexcept; + + winrt::Microsoft::ReactNative::ReactNativeHost const &Host() const noexcept; + std::wstring GetLastError() const noexcept; + + private: + winrt::Microsoft::ReactNative::ReactNativeHost m_host{nullptr}; + winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_queueController{nullptr}; + msrn::IReactDispatcher m_uiDispatcher{nullptr}; + mutable std::mutex m_errorMutex; + std::wstring m_lastError; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/TestReactPackageProvider.cpp b/vnext/Desktop.IntegrationTests/TestReactPackageProvider.cpp new file mode 100644 index 00000000000..951ed85de69 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestReactPackageProvider.cpp @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestReactPackageProvider.h" + +#include "Modules/TestAppState.h" +#include "Modules/TestDeviceInfo.h" +#include "Modules/TestModule.h" + +namespace Microsoft::React::Test { + +void TestReactPackageProvider::CreatePackage(msrn::IReactPackageBuilder const &packageBuilder) noexcept { + packageBuilder.AddTurboModule(L"DeviceInfo", winrt::Microsoft::ReactNative::MakeModuleProvider()); + packageBuilder.AddTurboModule(L"AppState", winrt::Microsoft::ReactNative::MakeModuleProvider()); + winrt::Microsoft::ReactNative::TryAddAttributedModule(packageBuilder, L"TestModule", true); +} + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/TestReactPackageProvider.h b/vnext/Desktop.IntegrationTests/TestReactPackageProvider.h new file mode 100644 index 00000000000..f464d8c0145 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestReactPackageProvider.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include + +namespace Microsoft::React::Test { + +namespace msrn = winrt::Microsoft::ReactNative; + +struct TestReactPackageProvider : winrt::implements { + void CreatePackage(msrn::IReactPackageBuilder const &packageBuilder) noexcept; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/TestTypeDependencies.dgml b/vnext/Desktop.IntegrationTests/TestTypeDependencies.dgml new file mode 100644 index 00000000000..c7d8289ba56 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestTypeDependencies.dgml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vnext/Desktop.IntegrationTests/TestUIDispatcher.cpp b/vnext/Desktop.IntegrationTests/TestUIDispatcher.cpp new file mode 100644 index 00000000000..0101c709ae5 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestUIDispatcher.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TestUIDispatcher.h" + +namespace Microsoft::React::Test { + +TestUIDispatcher::TestUIDispatcher(winrt::Microsoft::UI::Dispatching::DispatcherQueue const &dispatcherQueue) + : m_dispatcherQueue{dispatcherQueue} { + m_dispatcherQueue.TryEnqueue([self = get_strong()]() noexcept { self->m_threadId = GetCurrentThreadId(); }); +} + +bool TestUIDispatcher::HasThreadAccess() { + return m_threadId == GetCurrentThreadId(); +} + +void TestUIDispatcher::Post(msrn::ReactDispatcherCallback const &callback) { + m_dispatcherQueue.TryEnqueue([callback]() noexcept { callback(); }); +} + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/TestUIDispatcher.h b/vnext/Desktop.IntegrationTests/TestUIDispatcher.h new file mode 100644 index 00000000000..92123efb280 --- /dev/null +++ b/vnext/Desktop.IntegrationTests/TestUIDispatcher.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +#include +#include + +namespace Microsoft::React::Test { + +namespace msrn = winrt::Microsoft::ReactNative; + +struct TestUIDispatcher : public winrt::implements { + TestUIDispatcher(winrt::Microsoft::UI::Dispatching::DispatcherQueue const &dispatcherQueue); + + bool HasThreadAccess(); + void Post(msrn::ReactDispatcherCallback const &callback); + + private: + winrt::Microsoft::UI::Dispatching::DispatcherQueue m_dispatcherQueue; + DWORD m_threadId{0}; +}; + +} // namespace Microsoft::React::Test diff --git a/vnext/Desktop.IntegrationTests/WinRTActivationShims.cpp b/vnext/Desktop.IntegrationTests/WinRTActivationShims.cpp new file mode 100644 index 00000000000..548229bab7e --- /dev/null +++ b/vnext/Desktop.IntegrationTests/WinRTActivationShims.cpp @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// This file provides local implementations for WinRT static methods and +// constructors that are declared by the CppWinRT-generated headers with +// CppWinRTOptimized=true. In that mode, cppwinrt emits non-inline +// declarations expecting the symbols to be supplied by the producing DLL's +// import library. When those symbols are intentionally excluded from the +// DLL's DEF file, the linker cannot resolve them. +// +// The implementations below obtain activation factories directly from the +// react-native-win32.dll via its DllGetActivationFactory export. This +// bypasses RoGetActivationFactory and therefore does not require manifest +// or activation-context registration. + +#include +#include +#include +#include + +namespace { + +using DllGetActivationFactory_t = HRESULT(WINAPI *)(HSTRING, ::IActivationFactory **); + +DllGetActivationFactory_t GetDllGetActivationFactory() noexcept { + static auto pfn = reinterpret_cast( + ::GetProcAddress(::GetModuleHandleW(L"react-native-win32.dll"), "DllGetActivationFactory")); + return pfn; +} + +template +TFactory GetFactory(std::wstring_view className) { + winrt::hstring name{className}; + winrt::com_ptr<::IActivationFactory> factory; + winrt::check_hresult(GetDllGetActivationFactory()(static_cast(winrt::get_abi(name)), factory.put())); + return factory.as(); +} + +} // anonymous namespace + +namespace winrt::Microsoft::ReactNative { + +// ReactPropertyBagHelper statics ----------------------------------------- + +IReactPropertyNamespace ReactPropertyBagHelper::GetNamespace(param::hstring const &namespaceName) { + static auto factory = GetFactory(winrt::name_of()); + return factory.GetNamespace(namespaceName); +} + +IReactPropertyName ReactPropertyBagHelper::GetName(IReactPropertyNamespace const &ns, param::hstring const &localName) { + static auto factory = GetFactory(winrt::name_of()); + return factory.GetName(ns, localName); +} + +// ReactDispatcherHelper statics ------------------------------------------ + +IReactPropertyName ReactDispatcherHelper::UIDispatcherProperty() { + static auto factory = GetFactory(winrt::name_of()); + return factory.UIDispatcherProperty(); +} + +// ReactNativeHost default constructor ------------------------------------ + +ReactNativeHost::ReactNativeHost() + : ReactNativeHost(GetFactory(winrt::name_of()) + .ActivateInstance()) {} + +} // namespace winrt::Microsoft::ReactNative diff --git a/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json b/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json index e9f4f7afb6d..3f0c369c07c 100644 --- a/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json +++ b/vnext/Desktop.IntegrationTests/packages.experimentalwinui3.lock.json @@ -8,6 +8,12 @@ "resolved": "1.84.0", "contentHash": "4el2YP3cNJDVFPdzOso+LxGvdWP2rHxML4siq8VdonNypW2m4q503tHfCj6vK0L1UfxioE2hpFGb4ITEua73tg==" }, + "Microsoft.JavaScript.Hermes": { + "type": "Direct", + "requested": "[0.0.0-2512.22001-bc3d0ed7, )", + "resolved": "0.0.0-2512.22001-bc3d0ed7", + "contentHash": "aMuCKrIwkCAnT56+oKqmxgfIaAHlKRVt8IiG/jtMbG01QH1mLPwL7wP89jRMsYSJzikW96trqgpUllZZa3O+Qw==" + }, "Microsoft.Windows.CppWinRT": { "type": "Direct", "requested": "[2.0.230706.1, )", @@ -25,11 +31,6 @@ "resolved": "1.1.1", "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" }, - "Microsoft.JavaScript.Hermes": { - "type": "Transitive", - "resolved": "0.0.0-2512.22001-bc3d0ed7", - "contentHash": "aMuCKrIwkCAnT56+oKqmxgfIaAHlKRVt8IiG/jtMbG01QH1mLPwL7wP89jRMsYSJzikW96trqgpUllZZa3O+Qw==" - }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "1.1.1", diff --git a/vnext/Desktop.IntegrationTests/packages.lock.json b/vnext/Desktop.IntegrationTests/packages.lock.json index 10dfa3dfac0..f9a9523f5fd 100644 --- a/vnext/Desktop.IntegrationTests/packages.lock.json +++ b/vnext/Desktop.IntegrationTests/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "1.84.0", "contentHash": "4el2YP3cNJDVFPdzOso+LxGvdWP2rHxML4siq8VdonNypW2m4q503tHfCj6vK0L1UfxioE2hpFGb4ITEua73tg==" }, + "Microsoft.JavaScript.Hermes": { + "type": "Direct", + "requested": "[0.0.0-2512.22001-bc3d0ed7, )", + "resolved": "0.0.0-2512.22001-bc3d0ed7", + "contentHash": "aMuCKrIwkCAnT56+oKqmxgfIaAHlKRVt8IiG/jtMbG01QH1mLPwL7wP89jRMsYSJzikW96trqgpUllZZa3O+Qw==" + }, "Microsoft.Windows.CppWinRT": { "type": "Direct", "requested": "[2.0.230706.1, )", @@ -25,11 +31,6 @@ "resolved": "1.1.1", "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" }, - "Microsoft.JavaScript.Hermes": { - "type": "Transitive", - "resolved": "0.0.0-2512.22001-bc3d0ed7", - "contentHash": "aMuCKrIwkCAnT56+oKqmxgfIaAHlKRVt8IiG/jtMbG01QH1mLPwL7wP89jRMsYSJzikW96trqgpUllZZa3O+Qw==" - }, "Microsoft.SourceLink.Common": { "type": "Transitive", "resolved": "1.1.1", diff --git a/vnext/Desktop/React.Windows.Desktop.vcxproj b/vnext/Desktop/React.Windows.Desktop.vcxproj index 7b4d89f19b4..ab6c83d899d 100644 --- a/vnext/Desktop/React.Windows.Desktop.vcxproj +++ b/vnext/Desktop/React.Windows.Desktop.vcxproj @@ -270,7 +270,7 @@ - + diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index 8d77f9b65d3..c44789aaba5 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -428,7 +428,7 @@ - + diff --git a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets index 1a340beb7e0..e0803acbbaf 100644 --- a/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets +++ b/vnext/PropertySheets/External/Microsoft.ReactNative.Uwp.CSharpApp.targets @@ -25,7 +25,7 @@ - + - + diff --git a/vnext/PropertySheets/JSEngine.props b/vnext/PropertySheets/JSEngine.props index 7f1754cf43d..f31aa12b4de 100644 --- a/vnext/PropertySheets/JSEngine.props +++ b/vnext/PropertySheets/JSEngine.props @@ -7,8 +7,9 @@ true 0.0.0-2512.22001-bc3d0ed7 + Microsoft.JavaScript.Hermes $(PkgMicrosoft_JavaScript_Hermes) - $(NuGetPackageRoot)\Microsoft.JavaScript.Hermes\$(HermesVersion) + $(NuGetPackageRoot)\$(HermesPackageName)\$(HermesVersion) false true diff --git a/vnext/Scripts/Tfs/Invoke-WebRequestWithRetry.ps1 b/vnext/Scripts/Tfs/Invoke-WebRequestWithRetry.ps1 new file mode 100644 index 00000000000..4858238050d --- /dev/null +++ b/vnext/Scripts/Tfs/Invoke-WebRequestWithRetry.ps1 @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Downloads a file from a URI with retry logic. + +.PARAMETER Uri + The URI to download from. + +.PARAMETER OutFile + The output file path. + +.PARAMETER MaxRetries + Maximum number of download attempts. Default is 3. +#> +param( + [Parameter(Mandatory)] + [string]$Uri, + + [Parameter(Mandatory)] + [string]$OutFile, + + [int]$MaxRetries = 3 +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +for ($i = 1; $i -le $MaxRetries; $i++) { + try { + Write-Host "Downloading $OutFile (attempt $i of $MaxRetries)" + Invoke-WebRequest -Uri $Uri -OutFile $OutFile + return + } catch { + Write-Host "Attempt $i failed: $_" + if ($i -eq $MaxRetries) { throw } + Start-Sleep -Seconds (5 * $i) + } +} diff --git a/vnext/src-win/IntegrationTests/DummyTest.js b/vnext/src-win/IntegrationTests/DummyTest.js index 20626c88e2d..fb5e60a8e59 100644 --- a/vnext/src-win/IntegrationTests/DummyTest.js +++ b/vnext/src-win/IntegrationTests/DummyTest.js @@ -6,32 +6,10 @@ 'use strict'; -const React = require('react'); -const ReactNative = require('react-native'); +const {TurboModuleRegistry} = require('react-native'); +const TestModule = TurboModuleRegistry.get('TestModule'); -const {AppRegistry, StyleSheet, Text, View} = ReactNative; - -const {TestModule} = ReactNative.NativeModules; - -class DummyTest extends React.Component { - componentDidMount() { - TestModule.markTestPassed(true); - } - - render() { - return ( - - Some text - - ); - } +if (!TestModule) { + TestModule.markTestPassed(false, 'TestModule is not available'); } - -var styles = StyleSheet.create({ - container: {}, - row: {}, -}); - -AppRegistry.registerComponent('DummyTest', () => DummyTest); - -module.exports = DummyTest; +TestModule.markTestPassed(true);