diff --git a/.ado/build-template.yml b/.ado/build-template.yml index 9b0ced95c05..fdf809ff236 100644 --- a/.ado/build-template.yml +++ b/.ado/build-template.yml @@ -167,6 +167,7 @@ extends: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages + retryCountOnTaskFailure: 2 - template: .ado/templates/compute-beachball-branch-name.yml@self @@ -182,7 +183,12 @@ extends: - script: npx --yes midgard-yarn-strict@1.2.4 @rnw-scripts/beachball-config displayName: Strict yarn install @rnw-scripts/beachball-config - condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False')) + condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False'), ne(variables['Build.Reason'], 'PullRequest')) + retryCountOnTaskFailure: 2 + + - pwsh: .ado/scripts/install-yarn-dependencies.ps1 -Installer Strict -Scope '@rnw-scripts/beachball-config' + displayName: Strict yarn install @rnw-scripts/beachball-config + condition: and(succeeded(), eq(variables['detectScenario.isReleaseBuild'], 'False'), eq(variables['Build.Reason'], 'PullRequest')) retryCountOnTaskFailure: 2 - script: npx lage build --scope @rnw-scripts/prepare-release --scope @rnw-scripts/beachball-config diff --git a/.ado/pr-pipeline.yml b/.ado/pr-pipeline.yml index 44f7c28ab34..a977113ff11 100644 --- a/.ado/pr-pipeline.yml +++ b/.ado/pr-pipeline.yml @@ -14,6 +14,9 @@ pr: variables: - group: platform-override-zero-permission-token + # PR builds use the anonymous package proxy because 1ES network isolation blocks public registries. + - name: NPM_CONFIG_REGISTRY + value: https://packagefeedproxy.microsoft.io/npm/ extends: template: build-template.yml@self diff --git a/.ado/scripts/install-yarn-dependencies.ps1 b/.ado/scripts/install-yarn-dependencies.ps1 new file mode 100644 index 00000000000..e63cd94ea42 --- /dev/null +++ b/.ado/scripts/install-yarn-dependencies.ps1 @@ -0,0 +1,66 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateSet('Strict', 'Midgard', 'MidgardNpx')] + [string] $Installer, + + [string] $WorkingDirectory = '.', + + [string] $Scope +) + +$resolvedWorkingDirectory = (Resolve-Path $WorkingDirectory).Path +$lockFile = Join-Path $resolvedWorkingDirectory 'yarn.lock' +$originalLockFile = $null + +if ($env:BUILD_REASON -eq 'PullRequest') { + if (-not (Test-Path -LiteralPath $lockFile -PathType Leaf)) { + throw "Could not find yarn.lock in '$resolvedWorkingDirectory'." + } + + $registry = $env:NPM_CONFIG_REGISTRY + if ([string]::IsNullOrWhiteSpace($registry)) { + throw 'NPM_CONFIG_REGISTRY must be set for the network-isolated PR install.' + } + + $registry = $registry.TrimEnd('/') + '/' + $originalLockFile = [System.IO.File]::ReadAllBytes($lockFile) + $lockFileText = [System.Text.Encoding]::UTF8.GetString($originalLockFile) + $remappedLockFileText = $lockFileText. + Replace('https://registry.yarnpkg.com/', $registry). + Replace('https://registry.npmjs.org/', $registry) + + Write-Host "Temporarily routing yarn.lock package URLs through $registry" + [System.IO.File]::WriteAllText( + $lockFile, + $remappedLockFileText, + [System.Text.UTF8Encoding]::new($false) + ) +} + +try { + switch ($Installer) { + 'Strict' { + $arguments = @('--yes', 'midgard-yarn-strict@1.2.4') + if (-not [string]::IsNullOrWhiteSpace($Scope)) { + $arguments += $Scope + } + & npx @arguments + } + 'Midgard' { + & midgard-yarn --ignore-scripts --frozen-lockfile --cwd $resolvedWorkingDirectory + } + 'MidgardNpx' { + & npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd $resolvedWorkingDirectory + } + } + + if ($LASTEXITCODE -ne 0) { + throw "$Installer dependency installation exited with code $LASTEXITCODE." + } +} +finally { + if ($null -ne $originalLockFile) { + [System.IO.File]::WriteAllBytes($lockFile, $originalLockFile) + Write-Host 'Restored the original yarn.lock.' + } +} diff --git a/.ado/templates/react-native-init-windows.yml b/.ado/templates/react-native-init-windows.yml index df8113acc30..1846d15939f 100644 --- a/.ado/templates/react-native-init-windows.yml +++ b/.ado/templates/react-native-init-windows.yml @@ -32,6 +32,8 @@ parameters: steps: # Start npm test server - template: verdaccio-start.yml + parameters: + buildEnvironment: ${{ parameters.buildEnvironment }} - template: set-version-vars.yml parameters: @@ -46,7 +48,9 @@ steps: - ${{ if endsWith(parameters.template, '-app') }}: - script: | - $(Build.SourcesDirectory)\vnext\Scripts\creaternwapp.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli + if exist testcli rmdir /s /q testcli + if exist testcli exit /b 1 + call $(Build.SourcesDirectory)\vnext\Scripts\creaternwapp.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli displayName: Init new app project with creaternwapp.cmd workingDirectory: $(Agent.BuildDirectory) retryCountOnTaskFailure: 2 @@ -55,7 +59,9 @@ steps: - ${{ if endsWith(parameters.template, '-lib') }}: - script: | - $(Build.SourcesDirectory)\vnext\Scripts\creaternwlib.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli + if exist testcli rmdir /s /q testcli + if exist testcli exit /b 1 + call $(Build.SourcesDirectory)\vnext\Scripts\creaternwlib.cmd /rn $(reactNativeDevDependency) /rnw $(npmVersion) /t ${{ parameters.template }} /verdaccio testcli displayName: Init new lib project with creaternwlib.cmd workingDirectory: $(Agent.BuildDirectory) retryCountOnTaskFailure: 2 @@ -99,11 +105,36 @@ steps: workingDirectory: $(Agent.BuildDirectory)\testcli - pwsh: | - nuget.exe sources add -name TestFeed -source $(System.DefaultWorkingDirectory)\NugetTestFeed - nuget.exe sources remove -name react-native - nuget.exe sources remove -name Nuget.org - nuget.exe sources add -name Nuget.org -source https://api.nuget.org/v3/index.json - displayName: Add local NuGet test feed + $configPaths = @('NuGet.config') + if ('${{ parameters.template }}'.EndsWith('-lib')) { + $configPaths += 'example\NuGet.config' + } + + $sources = [ordered]@{ + TestFeed = '$(System.DefaultWorkingDirectory)\NugetTestFeed' + 'react-native' = 'https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json' + } + + foreach ($configPath in $configPaths) { + [xml] $config = Get-Content -LiteralPath $configPath + $packageSources = $config.configuration.packageSources + if (-not $packageSources) { + # The lib example inherits feeds from the root config. + Write-Host "Skipping $configPath (no packageSources element)" + continue + } + $packageSources.RemoveAll() + [void] $packageSources.AppendChild($config.CreateElement('clear')) + foreach ($source in $sources.GetEnumerator()) { + $element = $config.CreateElement('add') + $element.SetAttribute('key', $source.Key) + $element.SetAttribute('value', $source.Value) + [void] $packageSources.AppendChild($element) + } + $config.Save((Resolve-Path -LiteralPath $configPath)) + Write-Host "Updated $configPath" + } + displayName: Add compliant NuGet test feeds workingDirectory: $(Agent.BuildDirectory)\testcli - template: ../templates/run-windows-with-certificates.yml diff --git a/.ado/templates/verdaccio-start.yml b/.ado/templates/verdaccio-start.yml index 3a59d61c378..8619bd5c50d 100644 --- a/.ado/templates/verdaccio-start.yml +++ b/.ado/templates/verdaccio-start.yml @@ -3,9 +3,22 @@ parameters: - name: beachballPublish type: boolean default: true + - name: buildEnvironment + type: string + values: + - PullRequest + - Continuous steps: - - pwsh: start-process verdaccio.cmd -ArgumentList @('--config', './.ado/verdaccio/config.yaml') + - pwsh: | + $configPath = './.ado/verdaccio/config.yaml' + if ('${{ parameters.buildEnvironment }}' -eq 'PullRequest') { + $configPath = './.ado/verdaccio/config.pr.yaml' + } + if (-not (Test-Path -LiteralPath $configPath)) { + throw "Verdaccio config not found: $configPath" + } + start-process verdaccio.cmd -ArgumentList @('--config', $configPath) displayName: Launch test npm server (verdaccio) - script: node .ado/scripts/waitForVerdaccio.js diff --git a/.ado/templates/verdaccio-stop.yml b/.ado/templates/verdaccio-stop.yml index 17e4107396a..90c35f35141 100644 --- a/.ado/templates/verdaccio-stop.yml +++ b/.ado/templates/verdaccio-stop.yml @@ -18,5 +18,6 @@ steps: call yarn config delete npmRegistryServer call yarn config delete unsafeHttpWhitelist displayName: Reset npm/yarn config to stop poiting at local verdaccio server + condition: succeededOrFailed() env: YARN_ENABLE_IMMUTABLE_INSTALLS: false \ No newline at end of file diff --git a/.ado/templates/yarn-install.yml b/.ado/templates/yarn-install.yml index abdaef957ef..d05776c55dd 100644 --- a/.ado/templates/yarn-install.yml +++ b/.ado/templates/yarn-install.yml @@ -21,6 +21,7 @@ steps: script: | npm install --global yarn@1.22.22 midgard-yarn@1.23.34 verdaccio@6.7.2 displayName: Install Node base packages + retryCountOnTaskFailure: 2 # When using our own images, prefer the machine-installed version of # `midgard-yarn`. @@ -28,6 +29,12 @@ steps: - script: midgard-yarn --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} displayName: midgard-yarn (faster yarn install) retryCountOnTaskFailure: 2 + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + + - pwsh: .ado/scripts/install-yarn-dependencies.ps1 -Installer Midgard -WorkingDirectory '${{ parameters.workingDirectory }}' + displayName: midgard-yarn (faster yarn install) + retryCountOnTaskFailure: 2 + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) # If using an image we don't control, acquire a fixed version of midgard-yarn # before install @@ -35,3 +42,9 @@ steps: - script: npx --yes midgard-yarn@1.23.34 --ignore-scripts --frozen-lockfile --cwd ${{ parameters.workingDirectory }} displayName: midgard-yarn (faster yarn install) retryCountOnTaskFailure: 2 + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + + - pwsh: .ado/scripts/install-yarn-dependencies.ps1 -Installer MidgardNpx -WorkingDirectory '${{ parameters.workingDirectory }}' + displayName: midgard-yarn (faster yarn install) + retryCountOnTaskFailure: 2 + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) diff --git a/.ado/verdaccio/config.pr.yaml b/.ado/verdaccio/config.pr.yaml new file mode 100644 index 00000000000..5ce8caf6f11 --- /dev/null +++ b/.ado/verdaccio/config.pr.yaml @@ -0,0 +1,30 @@ +# PR network-isolation variant; keep shared settings in sync with config.yaml. +storage: ./storage +auth: + htpasswd: + file: ./htpasswd +uplinks: + npmFeed: + url: https://packagefeedproxy.microsoft.io/npm/ + max_fails: 40 + maxage: 30m + timeout: 60s + fail_timeout: 10m + cache: false + agent_options: + keepAlive: true + maxSockets: 40 + maxFreeSockets: 10 +publish: + allow_offline: true +packages: + '@*/*': + access: $all + publish: $all + proxy: npmFeed + '**': + access: $all + publish: $all + proxy: npmFeed +logs: + - {type: file, path: verdaccio.log, format: pretty, level: debug} diff --git a/.ado/verdaccio/config.yaml b/.ado/verdaccio/config.yaml index 6309a4660c8..da98ba3d7df 100644 --- a/.ado/verdaccio/config.yaml +++ b/.ado/verdaccio/config.yaml @@ -1,3 +1,4 @@ +# Keep shared Verdaccio settings in sync with config.pr.yaml. storage: ./storage auth: htpasswd: diff --git a/.ado/windows-vs-pr.yml b/.ado/windows-vs-pr.yml index 52958f7121b..42be702e5df 100644 --- a/.ado/windows-vs-pr.yml +++ b/.ado/windows-vs-pr.yml @@ -16,6 +16,9 @@ pr: variables: - group: platform-override-zero-permission-token + # PR builds use the anonymous package proxy because 1ES network isolation blocks public registries. + - name: NPM_CONFIG_REGISTRY + value: https://packagefeedproxy.microsoft.io/npm/ extends: template: build-template.yml@self diff --git a/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json b/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json new file mode 100644 index 00000000000..49c9ff24cec --- /dev/null +++ b/change/react-native-windows-0b30812f-d640-4e29-9a0e-fc3fc73cf5f7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Stabilize HTTP OPTIONS integration testing and network-isolated CLI validation", + "packageName": "react-native-windows", + "email": "anuagra@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts b/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts index b52afe565a3..44e0c8018e6 100644 --- a/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts +++ b/packages/e2e-test-app-fabric/test/FlatListComponentTest.test.ts @@ -74,7 +74,22 @@ describe('FlatList Tests', () => { await searchBoxBasic('555'); const component = await app.findElementByTestID('flatlist-basic'); await component.waitForDisplayed({timeout: 5000}); - const dump = await dumpVisualTree('flatlist-basic'); + let dump = await dumpVisualTree('flatlist-basic'); + await app.waitUntil( + async () => { + dump = await dumpVisualTree('flatlist-basic'); + const automationTree = JSON.stringify(dump['Automation Tree']); + return ( + automationTree.includes('Item 555 -') && + !automationTree.includes('LIST HEADER') + ); + }, + { + interval: 250, + timeout: 20000, + timeoutMsg: 'Filtered FlatList did not finish rendering.', + }, + ); expect(dump).toMatchSnapshot(); }); test('A FlatList has an onStartReached event', async () => { diff --git a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp index c20c173e86e..effc9dd2917 100644 --- a/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp +++ b/vnext/Desktop.IntegrationTests/HttpResourceIntegrationTests.cpp @@ -15,6 +15,7 @@ #include // Standard Library +#include #include #include "HttpServer.h" @@ -302,12 +303,18 @@ TEST_CLASS (HttpResourceIntegrationTest) { } TEST_METHOD(RequestOptionsSucceeds) { + constexpr int64_t optionsRequestId = 1; + constexpr int64_t getRequestId = 2; string url = "http://localhost:" + std::to_string(s_port); promise getResponsePromise; promise getDataPromise; promise optionsPromise; - string error; + std::atomic_bool getResponseCompleted{false}; + std::atomic_bool getDataCompleted{false}; + std::atomic_bool optionsCompleted{false}; + string getError; + string optionsError; IHttpResource::Response getResponse; IHttpResource::Response optionsResponse; string content; @@ -330,38 +337,66 @@ TEST_CLASS (HttpResourceIntegrationTest) { server->Start(); auto resource = IHttpResource::Make(); - resource->SetOnResponse([&getResponse, &getResponsePromise, &optionsResponse, &optionsPromise]( - int64_t, IHttpResource::Response callbackResponse) { - if (callbackResponse.StatusCode == static_cast(http::status::ok)) { - getResponse = callbackResponse; - getResponsePromise.set_value(); - } else if (callbackResponse.StatusCode == static_cast(http::status::partial_content)) { - optionsResponse = callbackResponse; - optionsPromise.set_value(); + resource->SetOnResponse([&getResponse, + &getResponsePromise, + &getResponseCompleted, + &optionsResponse, + &optionsPromise, + &optionsCompleted, + getRequestId, + optionsRequestId](int64_t requestId, IHttpResource::Response callbackResponse) { + if (requestId == getRequestId) { + if (!getResponseCompleted.exchange(true)) { + getResponse = callbackResponse; + getResponsePromise.set_value(); + } + } else if (requestId == optionsRequestId) { + if (!optionsCompleted.exchange(true)) { + optionsResponse = callbackResponse; + optionsPromise.set_value(); + } } }); - resource->SetOnData([&getDataPromise, &content](int64_t, string &&responseData) { - content = std::move(responseData); - - if (!content.empty()) - getDataPromise.set_value(); - }); - resource->SetOnError( - [&optionsPromise, &getResponsePromise, &getDataPromise, &error, &server](int64_t, string &&message, bool) { - error = std::move(message); - - optionsPromise.set_value(); + resource->SetOnData( + [&getDataPromise, &getDataCompleted, &content, getRequestId](int64_t requestId, string &&responseData) { + if (requestId == getRequestId && !getDataCompleted.exchange(true)) { + content = std::move(responseData); + getDataPromise.set_value(); + } + }); + resource->SetOnError([&optionsPromise, + &optionsCompleted, + &getResponsePromise, + &getResponseCompleted, + &getDataPromise, + &getDataCompleted, + &getError, + &optionsError, + getRequestId, + optionsRequestId](int64_t requestId, string &&message, bool) { + if (requestId == optionsRequestId && !optionsCompleted.exchange(true)) { + optionsError = std::move(message); + optionsPromise.set_value(); + } else if (requestId == getRequestId) { + const bool completeResponse = !getResponseCompleted.exchange(true); + const bool completeData = !getDataCompleted.exchange(true); + if (completeResponse || completeData) { + getError = std::move(message); + } + if (completeResponse) { getResponsePromise.set_value(); + } + if (completeData) { getDataPromise.set_value(); - - server->Stop(); - }); + } + } + }); //clang-format off resource->SendRequest( "OPTIONS", string{url}, - 0, /*requestId*/ + optionsRequestId, {}, /*headers*/ {}, /*data*/ "text", @@ -372,7 +407,7 @@ TEST_CLASS (HttpResourceIntegrationTest) { resource->SendRequest( "GET", std::move(url), - 0, /*requestId*/ + getRequestId, {}, /*headers*/ {}, /*data*/ "text", @@ -387,7 +422,10 @@ TEST_CLASS (HttpResourceIntegrationTest) { getDataPromise.get_future().wait(); server->Stop(); - Assert::AreEqual({}, error, L"Error encountered"); + Assert::AreEqual({}, optionsError, L"OPTIONS error encountered"); + Assert::AreEqual({}, getError, L"GET error encountered"); + Assert::AreEqual(static_cast(http::status::partial_content), optionsResponse.StatusCode); + Assert::AreEqual(static_cast(http::status::ok), getResponse.StatusCode); Assert::AreEqual(static_cast(1), optionsResponse.Headers.size()); for (auto header : optionsResponse.Headers) { if (header.first == "PreflightName") { diff --git a/vnext/Scripts/creaternwapp.cmd b/vnext/Scripts/creaternwapp.cmd index f5de6fbc61a..b335ff466ac 100644 --- a/vnext/Scripts/creaternwapp.cmd +++ b/vnext/Scripts/creaternwapp.cmd @@ -72,6 +72,9 @@ if not "%part%"=="" ( if %USE_VERDACCIO% equ 1 ( @echo creaternwapp.cmd: Setting npm to use verdaccio at http://localhost:4873 + set NPM_CONFIG_REGISTRY=http://localhost:4873 + set YARN_NPM_REGISTRY_SERVER=http://localhost:4873 + set YARN_UNSAFE_HTTP_WHITELIST=localhost call npm config set registry http://localhost:4873 ) diff --git a/vnext/Scripts/creaternwlib.cmd b/vnext/Scripts/creaternwlib.cmd index b2bab04879d..6e45f3bd0af 100644 --- a/vnext/Scripts/creaternwlib.cmd +++ b/vnext/Scripts/creaternwlib.cmd @@ -77,6 +77,9 @@ if not "%part%"=="" ( if %USE_VERDACCIO% equ 1 ( @echo creaternwlib.cmd: Setting npm to use verdaccio at http://localhost:4873 + set NPM_CONFIG_REGISTRY=http://localhost:4873 + set YARN_NPM_REGISTRY_SERVER=http://localhost:4873 + set YARN_UNSAFE_HTTP_WHITELIST=localhost call npm config set registry http://localhost:4873 )