diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 834c76d3dbdf..20d35cc675d1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -21,7 +21,7 @@ ] }, "microsoft.dotnet.xharness.cli": { - "version": "9.0.0-prerelease.24203.1", + "version": "9.0.0-prerelease.24224.1", "commands": [ "xharness" ] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 4c5a6b55a182..30539522ddb3 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -45,8 +45,10 @@ body: description: In what version do you see this issue? Run `dotnet workload list` to find your version. options: - + - 9.0.0-preview.3.10457 - 9.0.0-preview.2.10293 - 9.0.0-preview.1.9973 + - 8.0.21 SR4.1 - 8.0.20 SR4 - 8.0.14 SR3.1 - 8.0.10 SR3 @@ -115,8 +117,10 @@ body: - 8.0.10 SR3 - 8.0.14 SR3.1 - 8.0.20 SR4 + - 8.0.21 SR4.1 - 9.0.0-preview.1.9973 - 9.0.0-preview.2.10293 + - 9.0.0-preview.3.10457 validations: required: true - type: dropdown diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index 39ed64b00b3b..0fd58cf68d2f 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -463,13 +463,13 @@ configuration: then: - addReply: reply: >- - Thanks for the issue report @${issueAuthor}! This issue appears to be a problem with Visual Studio, so we ask that you use the VS feedback tool to report the issue. That way it will get to the routed to the team that owns this experience in VS. + Thanks for the issue report @${issueAuthor}! This issue appears to be a problem with Visual Studio (Code), so we ask that you use the VS feedback tool to report the issue. That way it will get to the routed to the team that owns this experience in VS (Code). - If you encounter a problem with Visual Studio, we want to know about it so that we can diagnose and fix it. By using the Report a Problem tool, you can collect detailed information about the problem, and send it to Microsoft with just a few button clicks. + If you encounter a problem with Visual Studio or the .NET MAUI VS Code Extension, we want to know about it so that we can diagnose and fix it. By using the Report a Problem tool, you can collect detailed information about the problem, and send it to Microsoft with just a few button clicks. - 1. Go to the [Visual Studio for Windows feedback tool](https://docs.microsoft.com/visualstudio/ide/how-to-report-a-problem-with-visual-studio?view=vs-2022) or [Visual Studio for Mac feedback tool](https://learn.microsoft.com/en-us/visualstudio/mac/report-a-problem?view=vsmac-2022) to report the issue + 1. Go to the [Visual Studio for Windows feedback tool](https://learn.microsoft.com/visualstudio/ide/how-to-report-a-problem-with-visual-studio) or [.NET MAUI VS Code Extension repository](https://github.com/microsoft/vscode-dotnettools/issues) to report the issue 2. Close this bug, and consider adding a link to the VS Feedback issue so that others can follow its activity there. description: Ask user to use VS Feedback for VS issues @@ -581,13 +581,48 @@ configuration: description: Remove 's/try-latest-version' when new reply from author comes in - if: - payloadType: Issues - - activitySenderHasPermission: - permission: Write - isAction: action: Opened + - or: + - isActivitySender: + user: PureWeen + issueAuthor: False + - isActivitySender: + user: mattleibow + issueAuthor: False + - isActivitySender: + user: rmarinho + issueAuthor: False + - isActivitySender: + user: jsuarezruiz + issueAuthor: False + - isActivitySender: + user: Redth + issueAuthor: False + - isActivitySender: + user: StephaneDelcroix + issueAuthor: False + - isActivitySender: + user: samhouts + issueAuthor: False + - isActivitySender: + user: jamesmontemagno + issueAuthor: False + - isActivitySender: + user: jonathanpeppers + issueAuthor: False + - isActivitySender: + user: rachelkang + issueAuthor: False + - isActivitySender: + user: Eilon + issueAuthor: False + - isActivitySender: + user: jfversluis + issueAuthor: False then: - addLabel: label: s/triaged - description: Add 's/triaged' label to issues opened by the core team, we assume these issues do not need triaging + description: Add 's/triaged' label to issues opened by the (core) team, we assume these issues do not need triaging onFailure: onSuccess: diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 0e7b77151016..3cf63182334d 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -20,13 +20,14 @@ permissions: jobs: main: runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dotnet' }} steps: - name: Checkout Actions uses: actions/checkout@v4 with: repository: "microsoft/vscode-github-triage-actions" path: ./actions - ref: cd16cd2aad6ba2da74bb6c6f7293adddd579a90e # locker action commit sha + ref: 066bee9cefa6f0b4bf306040ff36fc7d96a6d56d # locker action commit sha - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/similarIssues.yml b/.github/workflows/similarIssues.yml new file mode 100644 index 000000000000..ecba7b66952e --- /dev/null +++ b/.github/workflows/similarIssues.yml @@ -0,0 +1,38 @@ +name: GitGudSimilarIssues comments + +on: + issues: + types: [opened] + issue_comment: + types: [created] + +jobs: + getSimilarIssues: + runs-on: ubuntu-latest + if: >- + (github.event_name == 'issues' && github.event.action == 'opened') || + (github.event_name == 'issue_comment' && github.event.action == 'created' && startsWith(github.event.comment.body, '/similarissues')) + outputs: + message: ${{ steps.getBody.outputs.message }} + steps: + - id: getBody + uses: craigloewen-msft/GitGudSimilarIssues@main + with: + issueTitle: ${{ github.event.issue.title }} + issueBody: ${{ github.event.issue.body }} + repo: ${{ github.repository }} + similaritytolerance: "0.70" + add-comment: + needs: getSimilarIssues + runs-on: ubuntu-latest + permissions: + issues: write + if: needs.getSimilarIssues.outputs.message != '' + steps: + - name: Add comment + run: gh issue comment "$NUMBER" --repo "$REPO" --body "$BODY" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + BODY: ${{ needs.getSimilarIssues.outputs.message }} diff --git a/Microsoft.Maui-dev.sln b/Microsoft.Maui-dev.sln index c38c4fe99ab0..636df752bce6 100644 --- a/Microsoft.Maui-dev.sln +++ b/Microsoft.Maui-dev.sln @@ -241,9 +241,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Appium", "src\TestUt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{A307B624-48D4-494E-A70D-5B3CDF6620CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.SourceGen.UnitTests", "src\Controls\tests\SourceGen.UnitTests\Controls.SourceGen.UnitTests\Controls.SourceGen.UnitTests.csproj", "{06747B55-91DB-47F5-B7A2-56526C28A0D3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGen.UnitTests", "src\Controls\tests\SourceGen.UnitTests\SourceGen.UnitTests.csproj", "{BC7F7C82-694F-4B97-86FC-273FB3FACA25}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGen.UnitTests", "src\Controls\tests\SourceGen.UnitTests\SourceGen.UnitTests.csproj", "{06747B55-91DB-47F5-B7A2-56526C28A0D3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -618,10 +616,6 @@ Global {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {06747B55-91DB-47F5-B7A2-56526C28A0D3}.Release|Any CPU.Build.0 = Release|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BC7F7C82-694F-4B97-86FC-273FB3FACA25}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -734,7 +728,6 @@ Global {26379D0E-4D4D-48CA-94B1-A2C1972AB335} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {A307B624-48D4-494E-A70D-5B3CDF6620CF} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {06747B55-91DB-47F5-B7A2-56526C28A0D3} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} - {BC7F7C82-694F-4B97-86FC-273FB3FACA25} = {25D0D27A-C5FE-443D-8B65-D6C987F4A80E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/Microsoft.Maui.sln b/Microsoft.Maui.sln index 3ae8942ce0ec..b0454362fc6e 100644 --- a/Microsoft.Maui.sln +++ b/Microsoft.Maui.sln @@ -249,11 +249,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.Xaml.UnitTests.Int EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Controls.Sample.UITests", "src\Controls\samples\Controls.Sample.UITests\Controls.Sample.UITests.csproj", "{F39F75DC-671B-4649-8005-1929797B3217}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.Core", "src\TestUtils\src\UITest.Core\UITest.Core.csproj", "{352C2381-1DEC-4487-819D-340D1EA98FBE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Core", "src\TestUtils\src\UITest.Core\UITest.Core.csproj", "{352C2381-1DEC-4487-819D-340D1EA98FBE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.Appium", "src\TestUtils\src\UITest.Appium\UITest.Appium.csproj", "{8C8CD467-11F9-4A14-8AF3-047B2CFD19A7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Appium", "src\TestUtils\src\UITest.Appium\UITest.Appium.csproj", "{8C8CD467-11F9-4A14-8AF3-047B2CFD19A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{8050448A-E08F-4972-9B47-16042A5DFE82}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{8050448A-E08F-4972-9B47-16042A5DFE82}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlGallery.Android.Appium.UITests", "src\Compatibility\ControlGallery\test\Android.Appium.UITests\ControlGallery.Android.Appium.UITests.csproj", "{F748974F-A8E4-4659-801C-804B739D6326}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlGallery.iOS.Appium.UITests", "src\Compatibility\ControlGallery\test\iOS.Appium.UITests\ControlGallery.iOS.Appium.UITests.csproj", "{5923B35B-EA24-4B86-A384-9DAF9F2AFD56}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlGallery.Shared.Appium.UITests", "src\Compatibility\ControlGallery\test\Shared.Appium.UITests\ControlGallery.Shared.Appium.UITests.csproj", "{07D8D4B5-C89D-4BE3-A14A-17668358587C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -644,6 +650,18 @@ Global {8050448A-E08F-4972-9B47-16042A5DFE82}.Debug|Any CPU.Build.0 = Debug|Any CPU {8050448A-E08F-4972-9B47-16042A5DFE82}.Release|Any CPU.ActiveCfg = Release|Any CPU {8050448A-E08F-4972-9B47-16042A5DFE82}.Release|Any CPU.Build.0 = Release|Any CPU + {F748974F-A8E4-4659-801C-804B739D6326}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F748974F-A8E4-4659-801C-804B739D6326}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F748974F-A8E4-4659-801C-804B739D6326}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F748974F-A8E4-4659-801C-804B739D6326}.Release|Any CPU.Build.0 = Release|Any CPU + {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5923B35B-EA24-4B86-A384-9DAF9F2AFD56}.Release|Any CPU.Build.0 = Release|Any CPU + {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07D8D4B5-C89D-4BE3-A14A-17668358587C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -762,6 +780,9 @@ Global {352C2381-1DEC-4487-819D-340D1EA98FBE} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {8C8CD467-11F9-4A14-8AF3-047B2CFD19A7} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} {8050448A-E08F-4972-9B47-16042A5DFE82} = {7AC28763-9C68-4BF9-A1BA-25CBFFD2D15C} + {F748974F-A8E4-4659-801C-804B739D6326} = {DDBA9144-36FC-429E-99E1-2A64825434C1} + {5923B35B-EA24-4B86-A384-9DAF9F2AFD56} = {DDBA9144-36FC-429E-99E1-2A64825434C1} + {07D8D4B5-C89D-4BE3-A14A-17668358587C} = {DDBA9144-36FC-429E-99E1-2A64825434C1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B8ABEAD-D2B5-4370-A187-62B5ABE4EE50} diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9095a7dc28e6..d9bc8faf9223 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -143,17 +143,17 @@ https://github.com/dotnet/runtime 59edaad404d1b8e47080015ae8d0787f94c970df - + https://github.com/dotnet/xharness - 28af9496b0e260f7e66ec549b39f1410ee9743d1 + ec633d9ddbdb86dd3d772989889690821f790484 - + https://github.com/dotnet/xharness - 28af9496b0e260f7e66ec549b39f1410ee9743d1 + ec633d9ddbdb86dd3d772989889690821f790484 - + https://github.com/dotnet/xharness - 28af9496b0e260f7e66ec549b39f1410ee9743d1 + ec633d9ddbdb86dd3d772989889690821f790484 diff --git a/eng/Versions.props b/eng/Versions.props index c5f5d8e89a32..5f0ca83bf418 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -78,9 +78,9 @@ <_HarfBuzzSharpVersion>7.3.0 <_SkiaSharpNativeAssetsVersion>0.0.0-commit.e2c5c86249621857107c779af0f79b4d06613766.655 7.0.114 - 9.0.0-prerelease.24203.1 - 9.0.0-prerelease.24203.1 - 9.0.0-prerelease.24203.1 + 9.0.0-prerelease.24224.1 + 9.0.0-prerelease.24224.1 + 9.0.0-prerelease.24224.1 0.9.2 1.0.0.16 1.3.0 diff --git a/eng/cake/dotnet.cake b/eng/cake/dotnet.cake index d3c977ee0b07..9e129cf57cb0 100644 --- a/eng/cake/dotnet.cake +++ b/eng/cake/dotnet.cake @@ -222,6 +222,7 @@ Task("dotnet-test") "**/Controls.Core.UnitTests.csproj", "**/Controls.Core.Design.UnitTests.csproj", "**/Controls.Xaml.UnitTests.csproj", + "**/SourceGen.UnitTests.csproj", "**/Core.UnitTests.csproj", "**/Essentials.UnitTests.csproj", "**/Resizetizer.UnitTests.csproj", diff --git a/eng/pipelines/common/ui-tests-legacy-steps.yml b/eng/pipelines/common/ui-tests-legacy-steps.yml new file mode 100644 index 000000000000..45f154de55a4 --- /dev/null +++ b/eng/pipelines/common/ui-tests-legacy-steps.yml @@ -0,0 +1,146 @@ +parameters: + platform: '' # [ android, ios, windows, catalyst ] + path: '' # path to csproj + device: '' # the xharness device to use + cakeArgs: '' # additional cake args + app: '' #path to app to test + version: '' #the iOS version' + provisionatorChannel: 'latest' + agentPoolAccessToken: '' + targetSample: "dotnet-legacy-controlgallery" + configuration : "Release" + +steps: + - ${{ if eq(parameters.platform, 'ios')}}: + - bash: | + chmod +x $(System.DefaultWorkingDirectory)/eng/scripts/clean-bot.sh + $(System.DefaultWorkingDirectory)/eng/scripts/clean-bot.sh + displayName: 'Clean bot' + continueOnError: true + timeoutInMinutes: 60 + + - template: provision.yml + parameters: + skipProvisioning: ${{ eq(parameters.platform, 'windows') }} + skipAndroidSdks: ${{ ne(parameters.platform, 'android') }} + skipXcode: ${{ or(eq(parameters.platform, 'android'), eq(parameters.platform, 'windows')) }} + provisionatorChannel: ${{ parameters.provisionatorChannel }} + + - task: PowerShell@2 + condition: ne('${{ parameters.platform }}' , 'windows') + inputs: + targetType: 'inline' + script: | + defaults write -g NSAutomaticCapitalizationEnabled -bool false + defaults write -g NSAutomaticTextCompletionEnabled -bool false + defaults write -g NSAutomaticSpellingCorrectionEnabled -bool false + displayName: "Modify defaults" + continueOnError: true + + # AzDO hosted agents default to 1024x768; set something bigger for Windows UI tests + - task: ScreenResolutionUtility@1 + condition: eq('${{ parameters.platform }}' , 'windows') + inputs: + displaySettings: 'specific' + width: '1920' + height: '1080' + displayName: "Set screen resolution" + + - task: UseNode@1 + inputs: + version: "20.3.1" + displayName: "Install node" + + - bash: | + echo "##[group]Running ls -al $(npm root -g)" + ls -al $(npm root -g) + echo "##[endgroup]" + + echo "##[group]Running ls -al $(npm root -g)/appium" + ls -al $(npm root -g)/appium + echo "##[endgroup]" + + echo "##[group]Running ls -al $(npm root -g)/.appium-????????" + ls -al $(npm root -g)/.appium-???????? + echo "##[endgroup]" + + echo "##[group]Running ls -al $(npm root -g)/appium-doctor" + ls -al $(npm root -g)/appium-doctor + echo "##[endgroup]" + + echo "##[group]Running ls -al $(npm root -g)/.appium-doctor-????????" + ls -al $(npm root -g)/.appium-doctor-???????? + echo "##[endgroup]" + + echo "##[group]Running ps aux" + ps aux + echo "##[endgroup]" + displayName: "Debugging output" + continueOnError: true + condition: startsWith(variables['Agent.Name'], 'XAMBOT') + + # Clean up any leftover cached folders of appium and appium-doctor node modules + - bash: | + rm -rf $(npm root -g)/.appium-???????? + rm -rf $(npm root -g)/.appium-doctor-???????? + displayName: "Delete temp .appium-???????? and .appium-doctor-???????? folders" + continueOnError: true + + - pwsh: ./eng/scripts/appium-install.ps1 + displayName: "Install Appium (Drivers)" + continueOnError: false + retryCountOnTaskFailure: 1 + + - pwsh: ./build.ps1 --target=dotnet --configuration="${{ parameters.configuration }}" --verbosity=diagnostic + displayName: 'Install .NET' + retryCountOnTaskFailure: 2 + env: + DOTNET_TOKEN: $(dotnetbuilds-internal-container-read-token) + PRIVATE_BUILD: $(PrivateBuild) + + - pwsh: echo "##vso[task.prependpath]$(DotNet.Dir)" + displayName: 'Add .NET to PATH' + + - pwsh: ./build.ps1 --target=dotnet-buildtasks --configuration="${{ parameters.configuration }}" + displayName: 'Build the MSBuild Tasks' + + - pwsh: ./build.ps1 --target=${{ parameters.targetSample }} --configuration="${{ parameters.configuration }}" --${{ parameters.platform }} --verbosity=diagnostic --usenuget=false + displayName: 'Build the Legacy ControlGallery' + + - bash: | + if [ -f "$HOME/Library/Logs/CoreSimulator/*" ]; then rm -r $HOME/Library/Logs/CoreSimulator/*; fi + if [ -f "$HOME/Library/Logs/DiagnosticReports/*" ]; then rm -r $HOME/Library/Logs/DiagnosticReports/*; fi + displayName: Delete Old Simulator Logs + condition: ${{ eq(parameters.platform, 'ios') }} + continueOnError: true + + - pwsh: ./build.ps1 -Script eng/devices/${{ parameters.platform }}.cake --target=uitest --project="${{ parameters.path }}" --appproject="${{ parameters.app }}" --device="${{ parameters.device }}" --apiversion="${{ parameters.version }}" --configuration="${{ parameters.configuration }}" --results="$(TestResultsDirectory)" --binlog="$(LogDirectory)" ${{ parameters.cakeArgs }} --verbosity=diagnostic + displayName: $(Agent.JobName) + ${{ if ne(parameters.platform, 'android')}}: + retryCountOnTaskFailure: 1 + + - bash: | + suffix=$(date +%Y%m%d%H%M%S) + zip -9r "$(LogDirectory)/CoreSimulatorLog_${suffix}.zip" "$HOME/Library/Logs/CoreSimulator/" + zip -9r "$(LogDirectory)/DiagnosticReports_${suffix}.zip" "$HOME/Library/Logs/DiagnosticReports/" + displayName: Zip Simulator Logs + condition: ${{ eq(parameters.platform, 'ios') }} + continueOnError: true + + - task: PublishTestResults@2 + displayName: Publish the $(System.PhaseName) test results + condition: always() + inputs: + testResultsFormat: VSTest + testResultsFiles: '$(TestResultsDirectory)/*.trx' + testRunTitle: '$(System.PhaseName)' + failTaskOnFailedTests: true + + - task: PublishBuildArtifacts@1 + condition: always() + displayName: publish artifacts + + # This must always be placed as the last step in the job + - template: agent-rebooter/mac.v1.yml@yaml-templates + parameters: + AgentPoolAccessToken: ${{ parameters.agentPoolAccessToken }} diff --git a/eng/pipelines/common/ui-tests.yml b/eng/pipelines/common/ui-tests.yml index 639c8de45dfc..8865a818f0fa 100644 --- a/eng/pipelines/common/ui-tests.yml +++ b/eng/pipelines/common/ui-tests.yml @@ -5,11 +5,14 @@ parameters: macosPool: { } androidCompatibilityPool: { } iosCompatibilityPool: { } + androidLegacyPool: { } + iosLegacyPool: { } androidApiLevels: [ 30 ] iosVersions: [ 'latest' ] provisionatorChannel: 'latest' agentPoolAccessToken: '' runCompatibilityTests: false + runLegacyTests: true projects: - name: name desc: Human Description @@ -22,6 +25,10 @@ parameters: compatibilityAndroidTestProject: /optional/path/to/android.csproj compatibilityiOSTestProject: /optional/path/to/ios.csproj compatibilityiOSApp: /optional/path/to/app.csproj + legacyAndroidApp: /optional/path/to/app.csproj + legacyAndroidTestProject: /optional/path/to/android.csproj + legacyiOSTestProject: /optional/path/to/ios.csproj + legacyiOSApp: /optional/path/to/app.csproj stages: @@ -170,7 +177,6 @@ stages: provisionatorChannel: ${{ parameters.provisionatorChannel }} agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }} - - stage: ios_compatibility_ui_tests displayName: iOS Compatibility UITests dependsOn: [] @@ -206,3 +212,68 @@ stages: device: ios-simulator-64_${{ version }} provisionatorChannel: ${{ parameters.provisionatorChannel }} agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }} + - ${{ if eq(parameters.runLegacyTests, true) }}: + - stage: android_legacy_ui_tests + displayName: Android Legacy UITests + dependsOn: [] + jobs: + - ${{ each project in parameters.projects }}: + - ${{ if ne(project.android, '') }}: + - ${{ each api in parameters.androidApiLevels }}: + - ${{ if not(containsValue(project.androidApiLevelsExclude, api)) }}: + - job: android_legacy_ui_tests_${{ project.name }}_${{ api }} + timeoutInMinutes: 240 + workspace: + clean: all + displayName: ${{ coalesce(project.desc, project.name) }} (API ${{ api }}) + pool: ${{ parameters.androidLegacyPool }} + variables: + REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE) + steps: + - template: ui-tests-legacy-steps.yml + parameters: + platform: android + version: ${{ api }} + path: ${{ project.legacyAndroidTestProject }} + app: ${{ project.legacyAndroidApp }} + targetSample: "dotnet-legacy-controlgallery-android" + ${{ if eq(api, 27) }}: + device: android-emulator-32_${{ api }} + ${{ if not(eq(api, 27)) }}: + device: android-emulator-64_${{ api }} + provisionatorChannel: ${{ parameters.provisionatorChannel }} + agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }} + + - stage: ios_legacy_ui_tests + displayName: iOS Legacy UITests + dependsOn: [] + jobs: + - ${{ each project in parameters.projects }}: + - ${{ if ne(project.ios, '') }}: + - ${{ each version in parameters.iosVersions }}: + - ${{ if not(containsValue(project.iosVersionsExclude, version)) }}: + - job: ios_legacy_ui_tests_${{ project.name }}_${{ replace(version, '.', '_') }} + timeoutInMinutes: 240 + workspace: + clean: all + displayName: ${{ coalesce(project.desc, project.name) }} (v${{ version }}) + pool: ${{ parameters.iosLegacyPool }} + variables: + REQUIRED_XCODE: $(DEVICETESTS_REQUIRED_XCODE) + steps: + - template: ui-tests-legacy-steps.yml + parameters: + platform: ios + ${{ if eq(version, 'latest') }}: + version: 16.4 + ${{ if ne(version, 'latest') }}: + version: ${{ version }} + path: ${{ project.legacyiOSTestProject }} + app: ${{ project.legacyiOSApp }} + targetSample: "dotnet-legacy-controlgallery-ios" + ${{ if eq(version, 'latest') }}: + device: ios-simulator-64 + ${{ if ne(version, 'latest') }}: + device: ios-simulator-64_${{ version }} + provisionatorChannel: ${{ parameters.provisionatorChannel }} + agentPoolAccessToken: ${{ parameters.agentPoolAccessToken }} \ No newline at end of file diff --git a/eng/pipelines/ui-tests.yml b/eng/pipelines/ui-tests.yml index c2269ed23e66..46d4de15c7de 100644 --- a/eng/pipelines/ui-tests.yml +++ b/eng/pipelines/ui-tests.yml @@ -109,6 +109,23 @@ parameters: - macOS.Name -equals Ventura - macOS.Architecture -equals x64 + - name: androidLegacyPool + type: object + default: + name: $(androidTestsVmPool) + vmImage: $(androidTestsVmImage) + demands: + - macOS.Name -equals Ventura + - macOS.Architecture -equals x64 + + - name: iosLegacyPool + type: object + default: + name: $(iosTestsVmPool) + vmImage: $(iosTestsVmImage) + demands: + - macOS.Name -equals Ventura + - macOS.Architecture -equals x64 resources: repositories: @@ -128,6 +145,8 @@ stages: macosPool: ${{ parameters.macosPool }} androidCompatibilityPool: ${{ parameters.androidCompatibilityPool }} iosCompatibilityPool: ${{ parameters.iosCompatibilityPool }} + iosLegacyPool: ${{ parameters.iosLegacyPool }} + androidLegacyPool: ${{ parameters.androidLegacyPool }} agentPoolAccessToken: $(AgentPoolAccessToken) ${{ if or(parameters.BuildEverything, and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['System.TeamProject'], 'devdiv'))) }}: androidApiLevels: [ 30 ] @@ -153,4 +172,8 @@ stages: compatibilityAndroidTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/Android.UITests/Compatibility.ControlGallery.Android.UITests.csproj compatibilityiOSApp: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/src/iOS/Compatibility.ControlGallery.iOS.csproj compatibilityiOSTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/iOS.UITests/Compatibility.ControlGallery.iOS.UITests.csproj + legacyAndroidApp: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/src/Android/Compatibility.ControlGallery.Android.csproj + legacyAndroidTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj + legacyiOSApp: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/src/iOS/Compatibility.ControlGallery.iOS.csproj + legacyiOSTestProject: $(System.DefaultWorkingDirectory)/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj diff --git a/src/BlazorWebView/src/WindowsForms/BlazorWebView.cs b/src/BlazorWebView/src/WindowsForms/BlazorWebView.cs index 0af5f6fc0198..5a54464ce022 100644 --- a/src/BlazorWebView/src/WindowsForms/BlazorWebView.cs +++ b/src/BlazorWebView/src/WindowsForms/BlazorWebView.cs @@ -177,7 +177,7 @@ private void StartWebViewCoreIfPossible() } else { - appRootDir = Environment.CurrentDirectory; + appRootDir = AppContext.BaseDirectory; } var hostPageFullPath = Path.GetFullPath(Path.Combine(appRootDir, HostPage!)); // HostPage is nonnull because RequiredStartupPropertiesSet is checked above var contentRootDirFullPath = Path.GetDirectoryName(hostPageFullPath)!; diff --git a/src/BlazorWebView/src/Wpf/BlazorWebView.cs b/src/BlazorWebView/src/Wpf/BlazorWebView.cs index 98d6a68ca62f..af673607a2de 100644 --- a/src/BlazorWebView/src/Wpf/BlazorWebView.cs +++ b/src/BlazorWebView/src/Wpf/BlazorWebView.cs @@ -262,7 +262,7 @@ private void StartWebViewCoreIfPossible() } else { - appRootDir = Environment.CurrentDirectory; + appRootDir = AppContext.BaseDirectory; } var hostPageFullPath = Path.GetFullPath(Path.Combine(appRootDir, HostPage)); var contentRootDirFullPath = Path.GetDirectoryName(hostPageFullPath)!; diff --git a/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs b/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs index 2ec5edd2c0f9..0d555a4dbc19 100644 --- a/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs +++ b/src/Compatibility/ControlGallery/src/Android/FormsAppCompatActivity.cs @@ -4,6 +4,7 @@ using Android.Content; using Android.Content.PM; using Android.OS; +using Android.Runtime; using Java.Interop; using Microsoft.Maui.Controls.Compatibility.Platform.Android; using Microsoft.Maui.Controls.ControlGallery; @@ -28,6 +29,7 @@ namespace Microsoft.Maui.Controls.ControlGallery.Android DataScheme = "http", DataHost = App.AppName, DataPathPrefix = "/gallery/" ) ] + [Register("com.microsoft.mauicompatibilitygallery.MainActivity")] public partial class Activity1 : MauiAppCompatActivity { App App => Microsoft.Maui.Controls.Application.Current as App; diff --git a/src/Compatibility/ControlGallery/src/Core/GalleryPages/GroupedListContactsGallery.cs b/src/Compatibility/ControlGallery/src/Core/GalleryPages/GroupedListContactsGallery.cs index f33a3de14eab..0b009adf2da9 100644 --- a/src/Compatibility/ControlGallery/src/Core/GalleryPages/GroupedListContactsGallery.cs +++ b/src/Compatibility/ControlGallery/src/Core/GalleryPages/GroupedListContactsGallery.cs @@ -200,25 +200,11 @@ void AddContact(ObservableCollection contactGroups, Contact contact) InsertBasedOnSort(collection, contact, c => GetSortString(c)[0]); } - int IndexOf(IEnumerable elements, T element) - { - int i = 0; - foreach (T e in elements) - { - if (Equals(e, element)) - return i; - - i++; - } - - return -1; - } - void InsertBasedOnSort(IList items, T item, Func sortBy) { List newItems = new List(items); newItems.Add(item); - int index = IndexOf(newItems.OrderBy(sortBy), item); + int index = newItems.OrderBy(sortBy).IndexOf(item); items.Insert(index, item); } diff --git a/src/Compatibility/ControlGallery/src/Issues.Shared/Issue7534.cs b/src/Compatibility/ControlGallery/src/Issues.Shared/Issue7534.cs index e4d341f38830..98385bb38792 100644 --- a/src/Compatibility/ControlGallery/src/Issues.Shared/Issue7534.cs +++ b/src/Compatibility/ControlGallery/src/Issues.Shared/Issue7534.cs @@ -49,6 +49,7 @@ protected override void Init() #if UITEST && __ANDROID__ [Test] + [Compatibility.UITests.FailsOnMauiAndroid] public void ExpectingPageNotToBreak() { RunningApp.Screenshot("Test passed, label is showing as it should!"); diff --git a/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj new file mode 100644 index 000000000000..5d66c158ad15 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/ControlGallery.Android.Appium.UITests.csproj @@ -0,0 +1,43 @@ + + + + $(_MauiDotNetTfm) + enable + enable + true + UITests + $(DefineConstants);ANDROID + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Android.Appium.UITests/PlatformSpecificSampleTest.cs b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/PlatformSpecificSampleTest.cs new file mode 100644 index 000000000000..98435f47f3e8 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Android.Appium.UITests/PlatformSpecificSampleTest.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; + +namespace UITests; + +public class PlatformSpecificSampleTest : UITest +{ + public PlatformSpecificSampleTest(TestDevice testDevice) : base(testDevice) + { + } + + [Test] + public void SampleTest() + { + Driver?.GetScreenshot().SaveAsFile($"{nameof(SampleTest)}.png"); + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/AppiumServerHelper.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/AppiumServerHelper.cs new file mode 100644 index 000000000000..6165a3e9a341 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/AppiumServerHelper.cs @@ -0,0 +1,33 @@ +using OpenQA.Selenium.Appium.Service; + +namespace UITests; + +public static class AppiumServerHelper +{ + static AppiumLocalService? AppiumLocalService; + + public const string DefaultHostAddress = "127.0.0.1"; + public const int DefaultHostPort = 4723; + + public static void StartAppiumLocalServer(string host = DefaultHostAddress, + int port = DefaultHostPort) + { + if (AppiumLocalService is not null) + { + return; + } + + var builder = new AppiumServiceBuilder() + .WithIPAddress(host) + .UsingPort(port); + + // Start the server with the builder + AppiumLocalService = builder.Build(); + AppiumLocalService.Start(); + } + + public static void DisposeAppiumLocalServer() + { + AppiumLocalService?.Dispose(); + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/ControlGallery.Shared.Appium.UITests.csproj b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/ControlGallery.Shared.Appium.UITests.csproj new file mode 100644 index 000000000000..5c5de9da4a79 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/ControlGallery.Shared.Appium.UITests.csproj @@ -0,0 +1,36 @@ + + + + $(_MauiDotNetTfm) + False + False + UITests + + + + + + + + + + + + + + + + + + $(DefineConstants);ANDROID + + + + $(DefineConstants);IOS;IOSUITEST + + + + $(DefineConstants);WINDOWS;WINTEST + + + \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/GalleryQueries.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/GalleryQueries.cs new file mode 100644 index 000000000000..889551735add --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/GalleryQueries.cs @@ -0,0 +1,30 @@ +namespace UITests +{ + internal static class GalleryQueries + { + public const string ActivityIndicatorGallery = "ActivityIndicator Gallery"; + public const string BoxViewGallery = "BoxView Gallery"; + public const string ButtonGallery = "Button Gallery"; + public const string CheckBoxGallery = "CheckBox Gallery"; + public const string CollectionViewGallery = "CollectionView Gallery"; + public const string CarouselViewGallery = "CarouselView Gallery"; + public const string DatePickerGallery = "DatePicker Gallery"; + public const string EditorGallery = "Editor Gallery"; + public const string EntryGallery = "Entry Gallery"; + public const string FrameGallery = "Frame Gallery"; + public const string ImageGallery = "Image Gallery"; + public const string ImageButtonGallery = "Image Button Gallery"; + public const string LabelGallery = "Label Gallery"; + public const string ListViewGallery = "ListView Gallery"; + public const string PickerGallery = "Picker Gallery"; + public const string ProgressBarGallery = "ProgressBar Gallery"; + public const string RadioButtonGallery = "RadioButton Core Gallery"; + public const string ScrollViewGallery = "ScrollView Gallery"; + public const string SearchBarGallery = "SearchBar Gallery"; + public const string SliderGallery = "Slider Gallery"; + public const string StepperGallery = "Stepper Gallery"; + public const string SwitchGallery = "Switch Gallery"; + public const string TimePickerGallery = "TimePicker Gallery"; + public const string WebViewGallery = "WebView Gallery"; + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/IssuesUITest.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/IssuesUITest.cs new file mode 100644 index 000000000000..b8931ebf2c4e --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/IssuesUITest.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using UITest.Appium; + +namespace UITests +{ + public abstract class IssuesUITest : UITest + { + public IssuesUITest(TestDevice device) : base(device) { } + + protected override void FixtureSetup() + { + int retries = 0; + while (true) + { + try + { + base.FixtureSetup(); + NavigateToIssue(Issue); + break; + } + catch (Exception e) + { + TestContext.Error.WriteLine($">>>>> {DateTime.Now} The FixtureSetup threw an exception. Attempt {retries}/{SetupMaxRetries}.{Environment.NewLine}Exception details: {e}"); + if (retries++ < SetupMaxRetries) + { + Reset(); + } + else + { + throw; + } + } + } + } + + protected override void FixtureTeardown() + { + base.FixtureTeardown(); + try + { + this.Back(); + RunningApp.Tap("GoBackToGalleriesButton"); + } + catch (Exception e) + { + var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name; + TestContext.Error.WriteLine($">>>>> {DateTime.Now} The FixtureTeardown threw an exception during {name}.{Environment.NewLine}Exception details: {e}"); + } + } + + public abstract string Issue { get; } + + private void NavigateToIssue(string issue) + { + RunningApp.NavigateToIssues(); + + RunningApp.EnterText("SearchBarGo", issue); + + RunningApp.WaitForElement("SearchButton"); + RunningApp.Tap("SearchButton"); + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestAttributes.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestAttributes.cs new file mode 100644 index 000000000000..39b219bf0846 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestAttributes.cs @@ -0,0 +1,71 @@ +namespace UITests +{ + public static class Test + { + public enum Button + { + Clicked, + Command, + Text, + TextColor, + Font, + BorderWidth, + BorderColor, + BorderRadius, + Image, + Padding, + Pressed, + LineBreakMode + } + + public enum ImageButton + { + Source, + Aspect, + IsOpaque, + IsLoading, + AspectFill, + AspectFit, + Fill, + BorderColor, + CornerRadius, + BorderWidth, + Clicked, + Command, + Image, + Pressed, + Padding + } + + public enum VisualElement + { + IsEnabled, + Navigation, + InputTransparent, + Layout, + X, + Y, + AnchorX, + AnchorY, + TranslationX, + TranslationY, + Width, + Height, + Bounds, + Rotation, + RotationX, + RotationY, + Scale, + IsVisible, + Opacity, + BackgroundColor, + Background, + IsFocused, + Focus, + Unfocus, + Focused, + Unfocused, + Default + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestContextSetupFixture.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestContextSetupFixture.cs new file mode 100644 index 000000000000..b8631bc9026f --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestContextSetupFixture.cs @@ -0,0 +1,17 @@ +using UITest.Appium; + +// SetupFixture runs once for all tests under the same namespace, if placed outside the namespace it will run once for all tests in the assembly +namespace UITests +{ + public class TestContextSetupFixture : UITestContextSetupFixture + { + AppiumServerContext? _appiumServerContext; + + public override void Initialize() + { + _appiumServerContext = new AppiumServerContext(); + _appiumServerContext.CreateAndStartServer(); + _serverContext = _appiumServerContext; + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestDevice.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestDevice.cs new file mode 100644 index 000000000000..bb0f09240ac1 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/TestDevice.cs @@ -0,0 +1,10 @@ +namespace UITests +{ + public enum TestDevice + { + Windows, + Android, + iOS, + Mac + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/Tests/Issues/Issue11853.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/Tests/Issues/Issue11853.cs new file mode 100644 index 000000000000..1aa0f8291d18 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/Tests/Issues/Issue11853.cs @@ -0,0 +1,32 @@ +#if IOS +using NUnit.Framework; +using UITest.Appium; + +namespace UITests +{ + public class Issue11853 : IssuesUITest + { + const string Run = "Run"; + + public Issue11853(TestDevice testDevice) : base(testDevice) + { + } + + public override string Issue => "[Bug][iOS] Concurrent issue leading to crash in SemaphoreSlim.Release in ObservableItemsSource"; + + [Test] + [Category(UITestCategories.CollectionView)] + public void JustWhalingAwayOnTheCollectionViewWithAddsAndClearsShouldNotCrash() + { + RunningApp.WaitForElement(Run); + RunningApp.Tap(Run); + Task.Delay(5000).Wait(); + RunningApp.Tap(Run); + Task.Delay(5000).Wait(); + + // If we can still find the button, then we didn't crash + RunningApp.WaitForElement(Run); + } + } +} +#endif \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITest.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITest.cs new file mode 100644 index 000000000000..f129ab406969 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITest.cs @@ -0,0 +1,169 @@ +using NUnit.Framework; +using UITest.Core; +using VisualTestUtils; +using VisualTestUtils.MagickNet; + +namespace UITests +{ +#if ANDROID + [TestFixture(TestDevice.Android)] +#elif IOSUITEST + [TestFixture(TestDevice.iOS)] +#elif MACUITEST + [TestFixture(TestDevice.Mac)] +#elif WINTEST + [TestFixture(TestDevice.Windows)] +#else + [TestFixture(TestDevice.iOS)] + [TestFixture(TestDevice.Mac)] + [TestFixture(TestDevice.Windows)] + [TestFixture(TestDevice.Android)] +#endif + public abstract class UITest : UITestBase + { + protected const int SetupMaxRetries = 1; + readonly VisualRegressionTester _visualRegressionTester; + readonly IImageEditorFactory _imageEditorFactory; + readonly VisualTestContext _visualTestContext; + + protected UITest(TestDevice testDevice) : base(testDevice) + { + string? ciArtifactsDirectory = Environment.GetEnvironmentVariable("BUILD_ARTIFACTSTAGINGDIRECTORY"); + if (ciArtifactsDirectory != null) + ciArtifactsDirectory = Path.Combine(ciArtifactsDirectory, "Controls.AppiumTests"); + + string assemblyDirectory = Path.GetDirectoryName(System.AppDomain.CurrentDomain.BaseDirectory)!; + string projectRootDirectory = Path.GetFullPath(Path.Combine(assemblyDirectory, "..", "..", "..")); + _visualRegressionTester = new VisualRegressionTester(testRootDirectory: projectRootDirectory, + visualComparer: new MagickNetVisualComparer(), + visualDiffGenerator: new MagickNetVisualDiffGenerator(), + ciArtifactsDirectory: ciArtifactsDirectory); + + _imageEditorFactory = new MagickNetImageEditorFactory(); + _visualTestContext = new VisualTestContext(); + } + + public override IConfig GetTestConfig() + { + IConfig config = new Config(); + config.SetProperty("AppId", "com.microsoft.mauicompatibilitygallery"); + + switch (_testDevice) + { + case TestDevice.Android: + config.SetProperty("DeviceName", Environment.GetEnvironmentVariable("DEVICE_SKIN") ?? ""); + config.SetProperty("PlatformVersion", Environment.GetEnvironmentVariable("PLATFORM_VERSION") ?? ""); + config.SetProperty("Udid", Environment.GetEnvironmentVariable("DEVICE_UDID") ?? ""); + break; + case TestDevice.iOS: + config.SetProperty("DeviceName", Environment.GetEnvironmentVariable("DEVICE_NAME") ?? "iPhone X"); + config.SetProperty("PlatformVersion", Environment.GetEnvironmentVariable("PLATFORM_VERSION") ?? "17.0"); + config.SetProperty("Udid", Environment.GetEnvironmentVariable("DEVICE_UDID") ?? ""); + break; + } + + return config; + } + + public void VerifyScreenshot(string? name = null) + { + string deviceName = GetTestConfig().GetProperty("DeviceName") ?? string.Empty; + // Remove the XHarness suffix if present + deviceName = deviceName.Replace(" - created by XHarness", "", StringComparison.Ordinal); + + /* + Determine the environmentName, used as the directory name for visual testing snaphots. Here are the rules/conventions: + - Names are lower case, no spaces. + - By default, the name matches the platform (android, ios, windows, or mac). + - Each platform has a default device (or set of devices) - if the snapshot matches the default no suffix is needed (e.g. just ios). + - If tests are run on secondary devices that produce different snapshots, the device name is used as suffix (e.g. ios-iphonex). + - If tests are run on secondary devices with multiple OS versions that produce different snapshots, both device name and os version are + used as a suffix (e.g. ios-iphonex-16_4). We don't have any cases of this today but may eventually. The device name comes first here, + before os version, because most visual testing differences come from different sceen size (a device thing), not OS version differences, + but both can happen. + */ + string environmentName = string.Empty; + + switch (_testDevice) + { + case TestDevice.Android: + if (deviceName == "Nexus 5X") + { + environmentName = "android"; + } + else + { + Assert.Fail($"Android visual tests should be run on an Nexus 5X (API 30) emulator image, but the current device is '{deviceName}'. Follow the steps on the MAUI UI testing wiki."); + } + break; + + case TestDevice.iOS: + if (deviceName == "iPhone Xs (iOS 17.2)") + { + environmentName = "ios"; + } + else if (deviceName == "iPhone X (iOS 16.4)") + { + environmentName = "ios-iphonex"; + } + else + { + Assert.Fail($"iOS visual tests should be run on iPhone Xs (iOS 17.2) or iPhone X (iOS 16.4) simulator images, but the current device is '{deviceName}'. Follow the steps on the MAUI UI testing wiki."); + } + break; + + case TestDevice.Windows: + environmentName = "windows"; + break; + + case TestDevice.Mac: + // For now, ignore visual tests on Mac Catalyst since the Appium screenshot on Mac (unlike Windows) + // is of the entire screen, not just the app. Later when xharness relay support is in place to + // send a message to the MAUI app to get the screenshot, we can use that to just screenshot + // the app. + Assert.Ignore("MacCatalyst isn't supported yet for visual tests"); + break; + + default: + throw new NotImplementedException($"Unknown device type {_testDevice}"); + } + + name ??= TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name; + + byte[] screenshotPngBytes = RunningApp.Screenshot() ?? throw new InvalidOperationException("Failed to get screenshot"); + + var actualImage = new ImageSnapshot(screenshotPngBytes, ImageSnapshotFormat.PNG); + + // For Android and iOS, crop off the OS status bar at the top since it's not part of the + // app itself and contains the time, which always changes. For WinUI, crop off the title + // bar at the top as it varies slightly based on OS theme and is also not part of the app. + int cropFromTop = _testDevice switch + { + TestDevice.Android => 60, + TestDevice.iOS => environmentName == "ios-iphonex" ? 90 : 110, + TestDevice.Windows => 32, + _ => 0, + }; + + // For Android also crop the 3 button nav from the bottom, since it's not part of the + // app itself and the button color can vary (the buttons change clear briefly when tapped) + int cropFromBottom = _testDevice switch + { + TestDevice.Android => 125, + _ => 0, + }; + + if (cropFromTop > 0 || cropFromBottom > 0) + { + IImageEditor imageEditor = _imageEditorFactory.CreateImageEditor(actualImage); + (int width, int height) = imageEditor.GetSize(); + + imageEditor.Crop(0, cropFromTop, width, height - cropFromTop - cropFromBottom); + + actualImage = imageEditor.GetUpdatedImage(); + } + + _visualRegressionTester.VerifyMatchesSnapshot(name!, actualImage, environmentName: environmentName, testContext: _visualTestContext); + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestBase.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestBase.cs new file mode 100644 index 000000000000..936e5a1f7780 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestBase.cs @@ -0,0 +1,175 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using UITest.Core; + +namespace UITests +{ + public abstract class UITestBase : UITestContextBase + { + public UITestBase(TestDevice testDevice) + : base(testDevice) + { + } + + [SetUp] + public void RecordTestSetup() + { + var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name; + TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {name} Start"); + } + + [TearDown] + public void RecordTestTeardown() + { + var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name; + TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {name} Stop"); + } + + protected virtual void FixtureSetup() + { + var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name; + TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {nameof(FixtureSetup)} for {name}"); + } + + protected virtual void FixtureTeardown() + { + var name = TestContext.CurrentContext.Test.MethodName ?? TestContext.CurrentContext.Test.Name; + TestContext.Progress.WriteLine($">>>>> {DateTime.Now} {nameof(FixtureTeardown)} for {name}"); + } + + [TearDown] + public void UITestBaseTearDown() + { + if (App.AppState == ApplicationState.NotRunning) + { + SaveDeviceDiagnosticInfo(); + + Reset(); + FixtureSetup(); + + // Assert.Fail will immediately exit the test which is desirable as the app is not + // running anymore so we can't capture any UI structures or any screenshots + Assert.Fail("The app was expected to be running still, investigate as possible crash"); + } + + var testOutcome = TestContext.CurrentContext.Result.Outcome; + if (testOutcome == ResultState.Error || + testOutcome == ResultState.Failure) + { + SaveDeviceDiagnosticInfo(); + SaveUIDiagnosticInfo(); + } + } + + [OneTimeSetUp] + public void OneTimeSetup() + { + InitialSetup(UITestContextSetupFixture.ServerContext); + try + { + FixtureSetup(); + } + catch + { + SaveDeviceDiagnosticInfo(); + SaveUIDiagnosticInfo(); + throw; + } + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + var outcome = TestContext.CurrentContext.Result.Outcome; + + // We only care about setup failures as regular test failures will already do logging + if (outcome.Status == ResultState.SetUpFailure.Status && + outcome.Site == ResultState.SetUpFailure.Site) + { + SaveDeviceDiagnosticInfo(); + SaveUIDiagnosticInfo(); + } + + FixtureTeardown(); + } + + void SaveDeviceDiagnosticInfo([CallerMemberName] string? note = null) + { + var types = App.GetLogTypes().ToArray(); + TestContext.Progress.WriteLine($">>>>> {DateTime.Now} Log types: {string.Join(", ", types)}"); + + foreach (var logType in new[] { "logcat" }) + { + if (!types.Contains(logType, StringComparer.InvariantCultureIgnoreCase)) + continue; + + var logsPath = GetGeneratedFilePath($"AppLogs-{logType}.log", note); + if (logsPath is not null) + { + var entries = App.GetLogEntries(logType); + File.WriteAllLines(logsPath, entries); + + AddTestAttachment(logsPath, Path.GetFileName(logsPath)); + } + } + } + + void SaveUIDiagnosticInfo([CallerMemberName] string? note = null) + { + var screenshotPath = GetGeneratedFilePath("ScreenShot.png", note); + if (screenshotPath is not null) + { + _ = RunningApp.Screenshot(screenshotPath); + + AddTestAttachment(screenshotPath, Path.GetFileName(screenshotPath)); + } + + var pageSourcePath = GetGeneratedFilePath("PageSource.txt", note); + if (pageSourcePath is not null) + { + File.WriteAllText(pageSourcePath, App.ElementTree); + + AddTestAttachment(pageSourcePath, Path.GetFileName(pageSourcePath)); + } + } + + string? GetGeneratedFilePath(string filename, string? note = null) + { + // App could be null if UITestContext was not able to connect to the test process (e.g. port already in use etc...) + if (UITestContext is null) + return null; + + if (string.IsNullOrEmpty(note)) + note = "-"; + else + note = $"-{note}-"; + + filename = $"{Path.GetFileNameWithoutExtension(filename)}-{Guid.NewGuid().ToString("N")}{Path.GetExtension(filename)}"; + + var logDir = + Path.GetDirectoryName(Environment.GetEnvironmentVariable("APPIUM_LOG_FILE") ?? + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location))!; + + var name = + TestContext.CurrentContext.Test.MethodName ?? + TestContext.CurrentContext.Test.Name; + + return Path.Combine(logDir, $"{name}-{_testDevice}{note}{filename}"); + } + + void AddTestAttachment(string filePath, string? description = null) + { + try + { + TestContext.AddTestAttachment(filePath, description); + } + catch (FileNotFoundException e) when (e.Message == "Test attachment file path could not be found.") + { + // Add the file path to better troubleshoot when these errors occur + throw new FileNotFoundException($"Test attachment file path could not be found: '{filePath}' {description}", e); + } + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestCategories.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestCategories.cs new file mode 100644 index 000000000000..aa8c41a63837 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestCategories.cs @@ -0,0 +1,68 @@ +namespace UITests +{ + internal static class UITestCategories + { + public const string ViewBaseTests = "ViewBaseTests"; + public const string ActionSheet = "ActionSheet"; + public const string ActivityIndicator = "ActivityIndicator"; + public const string Animation = "Animation"; + public const string AutomationId = "AutomationID"; + public const string BoxView = "BoxView"; + public const string Button = "Button"; + public const string CarouselView = "CarouselView"; + public const string Cells = "Cells"; + public const string CheckBox = "CheckBox"; + public const string CollectionView = "CollectionView"; + public const string ContextActions = "ContextActions"; + public const string DatePicker = "DatePicker"; + public const string DragAndDrop = "DragAndDrop"; + public const string DisplayAlert = "DisplayAlert"; + public const string Editor = "Editor"; + public const string Entry = "Entry"; + public const string Frame = "Frame"; + public const string Image = "Image"; + public const string ImageButton = "ImageButton"; + public const string Label = "Label"; + public const string Layout = "Layout"; + public const string ListView = "ListView"; + public const string UwpIgnore = "UwpIgnore"; + public const string LifeCycle = "Lifecycle"; + public const string FlyoutPage = "FlyoutPage"; + public const string Picker = "Picker"; + public const string ProgressBar = "ProgressBar"; + public const string RequiresInternetConnection = "RequiresInternetConnection"; + public const string RootGallery = "RootGallery"; + public const string ScrollView = "ScrollView"; + public const string SearchBar = "SearchBar"; + public const string Slider = "Slider"; + public const string Stepper = "Stepper"; + public const string Switch = "Switch"; + public const string SwipeView = "SwipeView"; + public const string TableView = "TableView"; + public const string TimePicker = "TimePicker"; + public const string ToolbarItem = "ToolbarItem"; + public const string WebView = "WebView"; + public const string Maps = "Maps"; + public const string InputTransparent = "InputTransparent"; + public const string IsEnabled = "IsEnabled"; + public const string Gestures = "Gestures"; + public const string Navigation = "Navigation"; + public const string Effects = "Effects"; + public const string Focus = "Focus"; + public const string ManualReview = "ManualReview"; + public const string Performance = "Performance"; + public const string AppLinks = "AppLinks"; + public const string Shell = "Shell"; + public const string TabbedPage = "TabbedPage"; + public const string CustomHandlers = "CustomHandlers"; + public const string Page = "Page"; + public const string RefreshView = "RefreshView"; + public const string TitleView = "TitleView"; + public const string DisplayPrompt = "DisplayPrompt"; + public const string IndicatorView = "IndicatorView"; + public const string RadioButton = "RadioButton"; + public const string Shape = "Shape"; + public const string Accessibility = "Accessibility"; + public const string Brush = "Brush"; + } +} diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestContextBase.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestContextBase.cs new file mode 100644 index 000000000000..68f0a129e5dd --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestContextBase.cs @@ -0,0 +1,91 @@ +using OpenQA.Selenium.Appium; +using UITest.Appium; +using UITest.Core; + +namespace UITests +{ + public abstract class UITestContextBase + { + static IUIClientContext? UiTestContext; + IServerContext? _context; + protected TestDevice _testDevice; + + public UITestContextBase(TestDevice testDevice) + { + _testDevice = testDevice; + } + + public static IUIClientContext? UITestContext { get { return UiTestContext; } } + + protected AppiumDriver? Driver + { + get + { + if (App is AppiumApp app) + { + return app.Driver; + } + + return null; + } + } + + public TestDevice Device + { + get + { + return UITestContext == null + ? throw new InvalidOperationException($"Call {nameof(InitialSetup)} before accessing the {nameof(Device)} property.") + : UITestContext.Config.GetProperty("TestDevice"); + } + } + + public IApp App + { + get + { + return UITestContext == null + ? throw new InvalidOperationException($"Call {nameof(InitialSetup)} before accessing the {nameof(App)} property.") + : UITestContext.App; + } + } + + internal IApp RunningApp => App; + + public abstract IConfig GetTestConfig(); + + public void InitialSetup(IServerContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + InitialSetup(context, false); + } + + public void Reset() + { + if (_context == null) + { + throw new InvalidOperationException($"Cannot {nameof(Reset)} if {nameof(InitialSetup)} has not been called."); + } + + InitialSetup(_context, true); + } + + private void InitialSetup(IServerContext context, bool reset) + { + var testConfig = GetTestConfig(); + testConfig.SetProperty("TestDevice", _testDevice); + + // Check to see if we have a context already from a previous test and re-use it as creating the driver is expensive + if (reset || UiTestContext == null) + { + UiTestContext?.Dispose(); + UiTestContext = context.CreateUIClientContext(testConfig); + } + + if (UiTestContext == null) + { + throw new InvalidOperationException("Failed to get the driver."); + } + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestExtensions.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestExtensions.cs new file mode 100644 index 000000000000..c1b1b1461e67 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestExtensions.cs @@ -0,0 +1,90 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; +using System.Drawing; + +namespace UITests +{ + public static class UITestExtensions + { + const string GoToTestButtonId = "GoToTestButton"; + + public static void Back(this UITestContextBase testBase) + { + if (testBase.Device == TestDevice.Android) + { + var query = testBase.App.Query.ByAccessibilityId("Navigate up").First(); + query.Click(); + } + else if (testBase.Device == TestDevice.iOS || testBase.Device == TestDevice.Mac) + { + // Get the first NavigationBar we can find and the first button in it (the back button), index starts at 1 + var queryBy = testBase.App.Query.ByClass("XCUIElementTypeNavigationBar").First().ByClass("XCUIElementTypeButton").First(); + queryBy.Click(); + } + else + { + testBase.RunningApp.FindElement("NavigationViewBackButton").Click(); + } + } + + public static void NavigateToGallery(this IApp app, string page) + { + app.WaitForElement(GoToTestButtonId, "Timed out waiting for Go To Test button to appear", TimeSpan.FromMinutes(2)); + NavigateTo(app, page); + } + + public static void NavigateTo(this IApp app, string text) + { + app.WaitForElement("SearchBar"); + app.ClearText("SearchBar"); + if (!string.IsNullOrWhiteSpace(text)) + { + app.EnterText("SearchBar", text); + } + app.Tap(GoToTestButtonId); + + app.WaitForNoElement(GoToTestButtonId, "Timed out waiting for Go To Test button to disappear", TimeSpan.FromMinutes(1)); + } + + public static void NavigateToIssues(this IApp app) + { + app.WaitForElement(GoToTestButtonId, "Timed out waiting for Go To Test button to appear", TimeSpan.FromMinutes(2)); + + app.WaitForElement("SearchBar"); + app.ClearText("SearchBar"); + + app.Tap(GoToTestButtonId); + app.WaitForElement("TestCasesIssueList"); + } + + public static void IgnoreIfPlatforms(this UITestBase? test, IEnumerable devices, string? message = null) + { + foreach (var device in devices) + { + test?.IgnoreIfPlatform(device, message); + } + } + + public static void IgnoreIfPlatform(this UITestBase? test, TestDevice device, string? message = null) + { + if (test != null && test.Device == device) + { + if (string.IsNullOrEmpty(message)) + Assert.Ignore(); + else + Assert.Ignore(message); + } + } + + public static int CenterX(this Rectangle rect) + { + return rect.X + rect.Width / 2; + } + + public static int CenterY(this Rectangle rect) + { + return rect.Y + rect.Height / 2; + } + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestIgnoreAttributes.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestIgnoreAttributes.cs new file mode 100644 index 000000000000..4113ca8aaa5e --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/UITestIgnoreAttributes.cs @@ -0,0 +1,112 @@ +using NUnit.Framework; + +namespace UITests +{ + public class IgnoredDuringMoveToAppium : IgnoreAttribute + { + public IgnoredDuringMoveToAppium() : base(nameof(IgnoredDuringMoveToAppium)) + { + } + public IgnoredDuringMoveToAppium(string reason) : base(reason) + { + } + } + + public class FailsOnAllPlatforms : IgnoreAttribute + { + public FailsOnAllPlatforms() : base(nameof(FailsOnAndroid)) + { + } + public FailsOnAllPlatforms(string reason) : base(reason) + { + } + } + +#if ANDROID + public class FailsOnAndroid : IgnoreAttribute + { + public FailsOnAndroid() : base(nameof(FailsOnAndroid)) + { + } + public FailsOnAndroid(string reason) : base(reason) + { + } + } +#else + public class FailsOnAndroid : CategoryAttribute + { + public FailsOnAndroid() : base(nameof(FailsOnAndroid)) + { + } + public FailsOnAndroid(string name) : base(name) + { + } + } +#endif + +#if IOS + public class FailsOnIOS : IgnoreAttribute + { + public FailsOnIOS() : base(nameof(FailsOnIOS)) + { + } + public FailsOnIOS(string reason) : base(reason) + { + } + } +#else + public class FailsOnIOS : CategoryAttribute + { + public FailsOnIOS() : base(nameof(FailsOnIOS)) + { + } + public FailsOnIOS(string name) : base(name) + { + } + } +#endif + +#if MACCATALYST + public class FailsOnMac : IgnoreAttribute + { + public FailsOnMac() : base(nameof(FailsOnMac)) + { + } + public FailsOnMac(string reason) : base(reason) + { + } + } +#else + public class FailsOnMac : CategoryAttribute + { + public FailsOnMac() : base(nameof(FailsOnMac)) + { + } + public FailsOnMac(string name) : base(name) + { + } + } +#endif + +#if WINDOWS + public class FailsOnWindows : IgnoreAttribute + { + public FailsOnWindows() : base(nameof(FailsOnWindows)) + { + } + public FailsOnWindows(string reason) : base(reason) + { + } + } +#else + public class FailsOnWindows : CategoryAttribute + { + public FailsOnWindows() : base(nameof(FailsOnWindows)) + { + } + public FailsOnWindows(string name) : base(name) + { + } + } +#endif +} diff --git a/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/VisualTestContext.cs b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/VisualTestContext.cs new file mode 100644 index 000000000000..38598c27c489 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/Shared.Appium.UITests/VisualTestContext.cs @@ -0,0 +1,11 @@ +using NUnit.Framework; +using VisualTestUtils; + +namespace UITests +{ + public class VisualTestContext : ITestContext + { + public void AddTestAttachment(string filePath, string? description = null) => + TestContext.AddTestAttachment(filePath, description); + } +} \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj new file mode 100644 index 000000000000..4558c59d5333 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/ControlGallery.iOS.Appium.UITests.csproj @@ -0,0 +1,39 @@ + + + + $(_MauiDotNetTfm) + enable + enable + true + UITests + $(DefineConstants);IOS;IOSUITEST + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/PlatformSpecificSampleTest.cs b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/PlatformSpecificSampleTest.cs new file mode 100644 index 000000000000..98435f47f3e8 --- /dev/null +++ b/src/Compatibility/ControlGallery/test/iOS.Appium.UITests/PlatformSpecificSampleTest.cs @@ -0,0 +1,16 @@ +using NUnit.Framework; + +namespace UITests; + +public class PlatformSpecificSampleTest : UITest +{ + public PlatformSpecificSampleTest(TestDevice testDevice) : base(testDevice) + { + } + + [Test] + public void SampleTest() + { + Driver?.GetScreenshot().SaveAsFile($"{nameof(SampleTest)}.png"); + } +} \ No newline at end of file diff --git a/src/Compatibility/Core/src/Android/CollectionView/ObservableItemsSource.cs b/src/Compatibility/Core/src/Android/CollectionView/ObservableItemsSource.cs index eb68661752ff..9ca19ac3e8cb 100644 --- a/src/Compatibility/Core/src/Android/CollectionView/ObservableItemsSource.cs +++ b/src/Compatibility/Core/src/Android/CollectionView/ObservableItemsSource.cs @@ -137,7 +137,7 @@ void Move(NotifyCollectionChangedEventArgs args) void Add(NotifyCollectionChangedEventArgs args) { - var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]); + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); startIndex = AdjustPositionForHeader(startIndex); var count = args.NewItems.Count; @@ -178,7 +178,7 @@ void Remove(NotifyCollectionChangedEventArgs args) void Replace(NotifyCollectionChangedEventArgs args) { - var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]); + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); startIndex = AdjustPositionForHeader(startIndex); var newCount = args.NewItems.Count; @@ -229,21 +229,5 @@ internal object ElementAt(int index) return -1; } - - internal int IndexOf(object item) - { - if (_itemsSource is IList list) - return list.IndexOf(item); - - int count = 0; - foreach (var i in _itemsSource) - { - if (i == item) - return count; - count++; - } - - return -1; - } } } diff --git a/src/Compatibility/Core/src/iOS/CollectionView/ObservableItemsSource.cs b/src/Compatibility/Core/src/iOS/CollectionView/ObservableItemsSource.cs index f67f1d3ca9c1..c18e0e5e44a8 100644 --- a/src/Compatibility/Core/src/iOS/CollectionView/ObservableItemsSource.cs +++ b/src/Compatibility/Core/src/iOS/CollectionView/ObservableItemsSource.cs @@ -154,7 +154,7 @@ void Add(NotifyCollectionChangedEventArgs args) { var count = args.NewItems.Count; Count += count; - var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]); + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); // Queue up the updates to the UICollectionView Update(() => CollectionView.InsertItems(CreateIndexesFrom(startIndex, count)), args); @@ -185,7 +185,7 @@ void Replace(NotifyCollectionChangedEventArgs args) if (newCount == args.OldItems.Count) { - var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf(args.NewItems[0]); + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); // We are replacing one set of items with a set of equal size; we can do a simple item range update @@ -245,22 +245,6 @@ internal object ElementAt(int index) return -1; } - internal int IndexOf(object item) - { - if (_itemsSource is IList list) - return list.IndexOf(item); - - int count = 0; - foreach (var i in _itemsSource) - { - if (i == item) - return count; - count++; - } - - return -1; - } - void Update(Action update, NotifyCollectionChangedEventArgs args) { if (CollectionView.Hidden) diff --git a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs index cac261a064e4..ff54e348b2fe 100644 --- a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs +++ b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CorePageView.cs @@ -64,11 +64,11 @@ public override string ToString() new GalleryPageFactory(() => new ImageButtonCoreGalleryPage(), "Image Button Gallery"), new GalleryPageFactory(() => new ImageCoreGalleryPage(), "Image Gallery"), new GalleryPageFactory(() => new KeyboardScrollingGridGallery(), "Keyboard Scrolling Gallery - Grid with Star Row"), - new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Large Titles"), - new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Small Titles"), + new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Large Titles"), + new GalleryPageFactory(() => new KeyboardScrollingNonScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - NonScrolling Page / Small Titles"), new GalleryPageFactory(() => new KeyboardScrollingScrollingPageLargeTitlesGallery(), "Keyboard Scrolling Gallery - Scrolling Page / Large Titles"), new GalleryPageFactory(() => new KeyboardScrollingScrollingPageSmallTitlesGallery(), "Keyboard Scrolling Gallery - Scrolling Page / Small Titles"), - new GalleryPageFactory(() => new LabelCoreGalleryPage(), "Label Gallery"), + new GalleryPageFactory(() => new LabelCoreGalleryPage(), "Label Gallery"), new GalleryPageFactory(() => new ListViewCoreGalleryPage(), "ListView Gallery"), new GalleryPageFactory(() => new PickerCoreGalleryPage(), "Picker Gallery"), new GalleryPageFactory(() => new ProgressBarCoreGalleryPage(), "Progress Bar Gallery"), @@ -152,19 +152,34 @@ async Task PushPage(Page contentPage) } readonly Dictionary _titleToPage; - public async Task PushPage(string pageTitle) + public Task NavigateToGalleryPage(string pageTitle) { if (_titleToPage.TryGetValue(pageTitle.ToLowerInvariant(), out GalleryPageFactory pageFactory)) { var page = pageFactory.Realize(); + this.Window.Page = page; + return Task.FromResult(true); + } + + return Task.FromResult(false); + } - await PushPage(page); + public async Task NavigateToTest(string pageTitle) + { + var testCaseScreen = new TestCases.TestCaseScreen(); + if (testCaseScreen.TryToNavigateTo(pageTitle)) + { + return true; + } + else if (await NavigateToGalleryPage(pageTitle)) + { return true; } return false; } + public void FilterPages(string filter) { ItemsSource = string.IsNullOrWhiteSpace(filter) diff --git a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CoreRootPage.cs b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CoreRootPage.cs index a2d15d58edc3..4cc600e2319d 100644 --- a/src/Controls/samples/Controls.Sample.UITests/CoreViews/CoreRootPage.cs +++ b/src/Controls/samples/Controls.Sample.UITests/CoreViews/CoreRootPage.cs @@ -30,7 +30,7 @@ public CoreRootPage(Microsoft.Maui.Controls.Page rootPage) { if (!string.IsNullOrEmpty(searchBar.Text)) { - await corePageView.PushPage(searchBar.Text); + await corePageView.NavigateToTest(searchBar.Text); } else { diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/HideSoftInputOnTappedPage.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/HideSoftInputOnTappedPage.cs index f6d5d9dceb4d..06a29080b924 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/HideSoftInputOnTappedPage.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/HideSoftInputOnTappedPage.cs @@ -6,15 +6,19 @@ namespace Maui.Controls.Sample.Issues { [Issue(IssueTracker.None, 12003, "Hide Soft Input On Tapped Page", PlatformAffected.iOS | PlatformAffected.Android)] - public class HideSoftInputOnTappedPage : TestContentPage + public class HideSoftInputOnTappedPage : NavigationPage { - public HideSoftInputOnTappedPage() + public HideSoftInputOnTappedPage() : base(new StartingPage()) { + } - protected override void Init() + public class StartingPage : TestContentPage { - Content = new VerticalStackLayout() + + protected override void Init() + { + Content = new VerticalStackLayout() { new Button() { @@ -36,48 +40,48 @@ protected override void Init() } }; - (Content as VerticalStackLayout).Spacing = 6; - } + (Content as VerticalStackLayout).Spacing = 6; + } - public class TestPage : ContentPage - { - Label _isKeyboardOpen = new Label(); - public TestPage(bool hideSoftInputOnTapped) + public class TestPage : ContentPage { - this.HideSoftInputOnTapped = hideSoftInputOnTapped; - Title = "Hide Soft Input On Tapped Page"; - _isKeyboardOpen.Text = "Tap Page and Keyboard Should Close"; - _isKeyboardOpen.AutomationId = "EmptySpace"; + Label _isKeyboardOpen = new Label(); + public TestPage(bool hideSoftInputOnTapped) + { + this.HideSoftInputOnTapped = hideSoftInputOnTapped; + Title = "Hide Soft Input On Tapped Page"; + _isKeyboardOpen.Text = "Tap Page and Keyboard Should Close"; + _isKeyboardOpen.AutomationId = "EmptySpace"; - Entry entry = new Entry() { AutomationId = "Entry" }; + Entry entry = new Entry() { AutomationId = "Entry" }; - var checkbox = new CheckBox(); - checkbox.BindingContext = this; - checkbox.SetBinding( - CheckBox.IsCheckedProperty, - nameof(HideSoftInputOnTapped)); + var checkbox = new CheckBox(); + checkbox.BindingContext = this; + checkbox.SetBinding( + CheckBox.IsCheckedProperty, + nameof(HideSoftInputOnTapped)); - checkbox.AutomationId = "ToggleHideSoftInputOnTapped"; + checkbox.AutomationId = "ToggleHideSoftInputOnTapped"; - Entry dontHideKeyboardWhenTappingPage = new Entry() - { - Placeholder = "When this entry is focused tapping the page won't close the keyboard", - AutomationId = "DontHideKeyboardWhenTappingPage" - }; + Entry dontHideKeyboardWhenTappingPage = new Entry() + { + Placeholder = "When this entry is focused tapping the page won't close the keyboard", + AutomationId = "DontHideKeyboardWhenTappingPage" + }; - dontHideKeyboardWhenTappingPage - .Focused += (_, _) => checkbox.IsChecked = false; + dontHideKeyboardWhenTappingPage + .Focused += (_, _) => checkbox.IsChecked = false; - Entry hideKeyboardWhenTappingPage = new Entry() - { - Placeholder = "When this entry is focused tapping the page will close the keyboard", - AutomationId = "HideKeyboardWhenTappingPage" - }; + Entry hideKeyboardWhenTappingPage = new Entry() + { + Placeholder = "When this entry is focused tapping the page will close the keyboard", + AutomationId = "HideKeyboardWhenTappingPage" + }; - hideKeyboardWhenTappingPage - .Focused += (_, _) => checkbox.IsChecked = true; + hideKeyboardWhenTappingPage + .Focused += (_, _) => checkbox.IsChecked = true; - Content = new VerticalStackLayout() + Content = new VerticalStackLayout() { new HorizontalStackLayout() { @@ -90,6 +94,7 @@ public TestPage(bool hideSoftInputOnTapped) dontHideKeyboardWhenTappingPage, hideKeyboardWhenTappingPage }; + } } } } diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue11501.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue11501.cs index ae68e4fe475e..d5154fc75091 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue11501.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue11501.cs @@ -8,49 +8,57 @@ namespace Maui.Controls.Sample.Issues { [Issue(IssueTracker.Github, 11501, "Making Fragment Changes While App is Backgrounded Fails", PlatformAffected.Android)] - public class Issue11501 : TestContentPage + + public class Issue11501 : NavigationPage { - Func _currentTest; - Page _mainPage; - List _modalStack; - Window _window; - public Issue11501() + public Issue11501() : base(new TestPage()) { - Loaded += OnLoaded; } - private void OnLoaded(object sender, EventArgs e) - { - _window = Window; - _mainPage = Application.Current.MainPage; - _modalStack = Navigation.ModalStack.ToList(); - } - private async void OnWindowActivated(object sender, EventArgs e) + public class TestPage : TestContentPage { - DisconnectFromWindow(); - if (_currentTest is not null) + Func _currentTest; + Page _mainPage; + List _modalStack; + Window _window; + public TestPage() { - await Task.Yield(); - await _currentTest(); - _currentTest = null; + Loaded += OnLoaded; } - } - void ConnectToWindow() - { - _window.Stopped -= OnWindowActivated; - _window.Stopped += OnWindowActivated; - } + private void OnLoaded(object sender, EventArgs e) + { + _window = Window; + _mainPage = Application.Current.MainPage; + _modalStack = Navigation.ModalStack.ToList(); + } - void DisconnectFromWindow() - { - _window.Stopped -= OnWindowActivated; - } + private async void OnWindowActivated(object sender, EventArgs e) + { + DisconnectFromWindow(); + if (_currentTest is not null) + { + await Task.Yield(); + await _currentTest(); + _currentTest = null; + } + } - protected override void Init() - { - Content = new VerticalStackLayout() + void ConnectToWindow() + { + _window.Stopped -= OnWindowActivated; + _window.Stopped += OnWindowActivated; + } + + void DisconnectFromWindow() + { + _window.Stopped -= OnWindowActivated; + } + + protected override void Init() + { + Content = new VerticalStackLayout() { new Button() { @@ -151,14 +159,14 @@ protected override void Init() }) }, }; - } + } - ContentPage CreateDestinationPage() - { - return new ContentPage() + ContentPage CreateDestinationPage() { - Title = "Test", - Content = new VerticalStackLayout() + return new ContentPage() + { + Title = "Test", + Content = new VerticalStackLayout() { new Button() { @@ -177,7 +185,8 @@ ContentPage CreateDestinationPage() }) } } - }; + }; + } } } -} +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue14829.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue14829.cs index f8c2bf458aa4..cfedb9c0728a 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue14829.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue14829.cs @@ -1,4 +1,5 @@ -using Microsoft.Maui.Controls; +using System; +using Microsoft.Maui.Controls; using Microsoft.Maui.Graphics; namespace Maui.Controls.Sample.Issues @@ -9,12 +10,11 @@ public class Issue14829 : TestContentPage protected override void Init() { var navPage = new NavigationPage(new MainPage()); - NavigatedTo += Issue14829_NavigatedTo; + Loaded += OnLoaded; - async void Issue14829_NavigatedTo(object sender, NavigatedToEventArgs e) + async void OnLoaded(object sender, EventArgs e) { - NavigatedTo -= Issue14829_NavigatedTo; - + Loaded -= OnLoaded; await Navigation.PushModalAsync(navPage); } } diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue16499.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue16499.cs index c886c671ff00..d73801e944b9 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue16499.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue16499.cs @@ -3,32 +3,39 @@ namespace Maui.Controls.Sample.Issues { [Issue(IssueTracker.Github, 16499, "Crash when using NavigationPage.TitleView and Restarting App", PlatformAffected.Android)] - public class Issue16499 : TestContentPage + public class Issue16499 : NavigationPage { - protected override void Init() + public Issue16499() : base(new MainPage()) { - var contentPage = new ContentPage(); - var navPage = new NavigationPage(contentPage); + } - NavigationPage.SetTitleView(contentPage, new Label()); + public class MainPage : ContentPage + { + public MainPage() : base() + { + var contentPage = new ContentPage(); + var navPage = new NavigationPage(contentPage); - NavigatedTo += Issue16499_NavigatedTo; + NavigationPage.SetTitleView(contentPage, new Label()); - async void Issue16499_NavigatedTo(object sender, NavigatedToEventArgs e) - { - NavigatedTo -= Issue16499_NavigatedTo; - await Navigation.PushModalAsync(navPage); - await Navigation.PopModalAsync(); - await Navigation.PushModalAsync(navPage); - await Navigation.PopModalAsync(); - this.Content = new VerticalStackLayout() + NavigatedTo += Issue16499_NavigatedTo; + + async void Issue16499_NavigatedTo(object sender, NavigatedToEventArgs e) { - new Label() + NavigatedTo -= Issue16499_NavigatedTo; + await Navigation.PushModalAsync(navPage); + await Navigation.PopModalAsync(); + await Navigation.PushModalAsync(navPage); + await Navigation.PopModalAsync(); + Content = new VerticalStackLayout() { - Text = "If the app didn't crash this test was a success", - AutomationId = "SuccessLabel" - } - }; + new Label() + { + Text = "If the app didn't crash this test was a success", + AutomationId = "SuccessLabel" + } + }; + } } } } diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue17022.xaml b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue17022.xaml index 818a40baa44a..8fe7409889c1 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue17022.xaml +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue17022.xaml @@ -4,28 +4,29 @@ AutomationId="NavBarTranslucentPage" x:Class="Maui.Controls.Sample.Issues.Issue17022" xmlns:ios="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;assembly=Microsoft.Maui.Controls"> - - - + + + - \ No newline at end of file + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issues16321.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issues16321.xaml.cs index dca39801fe85..ea75962617fe 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/Issues16321.xaml.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issues16321.xaml.cs @@ -14,9 +14,17 @@ namespace Maui.Controls.Sample.Issues { [XamlCompilation(XamlCompilationOptions.Compile)] [Issue(IssueTracker.Github, 16321, "Alerts Open on top of current presented view", PlatformAffected.All)] + public class Issue16321NavPage : NavigationPage + { + public Issue16321NavPage() : base(new Issue16321()) + { + + } + } + public partial class Issue16321 : ContentPage { - public Issue16321() + public Issue16321() : base() { InitializeComponent(); } @@ -67,10 +75,10 @@ async void OpenAlertWithNewUIWindow(System.Object sender, System.EventArgs e) this.RemoveLogicalChild(page); } #else - async void OpenAlertWithNewUIWindow(System.Object sender, System.EventArgs e) - { - await this.DisplayAlert("hello", "message", "Cancel"); - } + async void OpenAlertWithNewUIWindow(System.Object sender, System.EventArgs e) + { + await this.DisplayAlert("hello", "message", "Cancel"); + } #endif } -} +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/ScrollViewIsEnabled.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/ScrollViewIsEnabled.cs index 600481cb1131..4256187fd02c 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/ScrollViewIsEnabled.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/ScrollViewIsEnabled.cs @@ -8,80 +8,87 @@ namespace Maui.Controls.Sample.Issues // ScrollViewInitiallyNotEnabled (src\Compatibility\ControlGallery\src\Issues.Shared\ScrollViewIsEnabled.cs) // ScrollViewInitiallyNotEnabledThenEnabled (src\Compatibility\ControlGallery\src\Issues.Shared\ScrollViewIsEnabled.cs) [Issue(IssueTracker.None, 0112358, "ScrollView set to disabled will still allow scrolling", PlatformAffected.All)] - public class ScrollViewIsEnabled : ContentPage + public class ScrollViewIsEnabled : NavigationPage { - const string InitiallyEnabled = "Initially Enabled"; - const string InitiallyNotEnabled = "Initially Not Enabled"; - const string ToggleButton = "ToggleButton"; - const string ScrollView = "TheScrollView"; - const string FirstItem = "FirstItem"; - const string Success = "Success"; - - public ScrollViewIsEnabled() + public ScrollViewIsEnabled() : base(new TestPage()) { - var initiallyEnabled = new Button { AutomationId = "InitiallyEnabled", Text = InitiallyEnabled }; - initiallyEnabled.Clicked += (sender, args) => { Navigation.PushAsync(ScrollViewTestPage(true)); }; + } - var initiallyNotEnabled = new Button { AutomationId= "InitiallyNotEnabled", Text = InitiallyNotEnabled }; - initiallyNotEnabled.Clicked += (sender, args) => { Navigation.PushAsync(ScrollViewTestPage(false)); }; + public class TestPage : ContentPage + { + const string InitiallyEnabled = "Initially Enabled"; + const string InitiallyNotEnabled = "Initially Not Enabled"; + const string ToggleButton = "ToggleButton"; + const string ScrollView = "TheScrollView"; + const string FirstItem = "FirstItem"; + const string Success = "Success"; + + public TestPage() + { + var initiallyEnabled = new Button { AutomationId = "InitiallyEnabled", Text = InitiallyEnabled }; + initiallyEnabled.Clicked += (sender, args) => { Navigation.PushAsync(ScrollViewTestPage(true)); }; - var layout = new StackLayout { Children = { initiallyNotEnabled, initiallyEnabled } }; + var initiallyNotEnabled = new Button { AutomationId = "InitiallyNotEnabled", Text = InitiallyNotEnabled }; + initiallyNotEnabled.Clicked += (sender, args) => { Navigation.PushAsync(ScrollViewTestPage(false)); }; - Content = layout; - } + var layout = new StackLayout { Children = { initiallyNotEnabled, initiallyEnabled } }; - static ContentPage ScrollViewTestPage(bool initiallyEnabled) - { - var scrollViewContents = new StackLayout(); - scrollViewContents.Children.Add(new Label { AutomationId = FirstItem, Text = FirstItem }); + Content = layout; + } - for (int n = 0; n < 100; n++) + static ContentPage ScrollViewTestPage(bool initiallyEnabled) { - scrollViewContents.Children.Add(new Label { AutomationId = $"Item{n + 1}", Text = $"Item{n + 1}" }); - } + var scrollViewContents = new StackLayout(); + scrollViewContents.Children.Add(new Label { AutomationId = FirstItem, Text = FirstItem }); - var sv = new ScrollView { Content = scrollViewContents, IsEnabled = initiallyEnabled, AutomationId = ScrollView }; - var layout = new Grid { Margin = new Thickness(5, 40, 5, 0) }; + for (int n = 0; n < 100; n++) + { + scrollViewContents.Children.Add(new Label { AutomationId = $"Item{n + 1}", Text = $"Item{n + 1}" }); + } - layout.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto }); - layout.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto }); - layout.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto }); - layout.RowDefinitions.Add(new RowDefinition()); + var sv = new ScrollView { Content = scrollViewContents, IsEnabled = initiallyEnabled, AutomationId = ScrollView }; + var layout = new Grid { Margin = new Thickness(5, 40, 5, 0) }; - var toggleButton = new Button { Text = $"Toggle IsEnabled (currently {sv.IsEnabled})", AutomationId = ToggleButton }; + layout.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto }); + layout.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto }); + layout.RowDefinitions.Add(new RowDefinition() { Height = GridLength.Auto }); + layout.RowDefinitions.Add(new RowDefinition()); - toggleButton.Clicked += (sender, args) => - { - sv.IsEnabled = !sv.IsEnabled; - toggleButton.Text = $"Toggle IsEnabled (currently {sv.IsEnabled})"; - }; + var toggleButton = new Button { Text = $"Toggle IsEnabled (currently {sv.IsEnabled})", AutomationId = ToggleButton }; - var instructions = new Label - { - Text = @"Attempt to scroll the ScrollView below. + toggleButton.Clicked += (sender, args) => + { + sv.IsEnabled = !sv.IsEnabled; + toggleButton.Text = $"Toggle IsEnabled (currently {sv.IsEnabled})"; + }; + + var instructions = new Label + { + Text = @"Attempt to scroll the ScrollView below. If 'IsEnabled' is false and the ScrollView scrolls, this test has failed. If 'IsEnabled' is true and the ScrollView does not scroll, this test has failed. Use the toggle button to check both values of 'IsEnabled'." - }; + }; - var success = new Label - { - AutomationId = Success - }; + var success = new Label + { + AutomationId = Success + }; - layout.Children.Add(instructions); - layout.Children.Add(toggleButton); - layout.Children.Add(success); - layout.Children.Add(sv); + layout.Children.Add(instructions); + layout.Children.Add(toggleButton); + layout.Children.Add(success); + layout.Children.Add(sv); - Grid.SetRow(instructions, 0); - Grid.SetRow(toggleButton, 1); - Grid.SetRow(success, 2); - Grid.SetRow(sv, 3); + Grid.SetRow(instructions, 0); + Grid.SetRow(toggleButton, 1); + Grid.SetRow(success, 2); + Grid.SetRow(sv, 3); - sv.Scrolled += (sender, args) => success.Text = Success; + sv.Scrolled += (sender, args) => success.Text = Success; - return new ContentPage { Content = layout }; + return new ContentPage { Content = layout }; + } } } } \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/XFIssue2681.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/XFIssue2681.cs index f0976d6d2e37..db781eec1417 100644 --- a/src/Controls/samples/Controls.Sample.UITests/Issues/XFIssue2681.cs +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/XFIssue2681.cs @@ -9,54 +9,61 @@ namespace Maui.Controls.Sample.Issues; [Issue(IssueTracker.None, 2681, "[UWP] Label inside Listview gets stuck inside infinite loop", PlatformAffected.UWP)] -public class XFIssue2681 : TestContentPage + +public class XFIssue2681 : NavigationPage { - const string NavigateToPage = "Click Me."; - protected override void Init() + public XFIssue2681() : base(new TestPage()) { - Content = new Button() - { - Text = NavigateToPage, - AutomationId = "ClickMe", - Command = new Command(async () => await this.Navigation.PushAsync(new FreezeMe())) - }; } - public partial class FreezeMe : ContentPage + public class TestPage : TestContentPage { - public List Items { get; set; } - - public FreezeMe() + const string NavigateToPage = "Click Me."; + protected override void Init() { - this.BindingContext = this; - var lv = new ListView() + Content = new Button() { - Margin = new Thickness(20, 5, 5, 5) + Text = NavigateToPage, + AutomationId = "ClickMe", + Command = new Command(async () => await this.Navigation.PushAsync(new FreezeMe())) }; + } + + public partial class FreezeMe : ContentPage + { + public List Items { get; set; } - lv.ItemTemplate = new DataTemplate(() => + public FreezeMe() { - var label = new Label() { Text = "sassifrass" }; - label.SetBinding(Label.TextProperty, "."); - label.SetBinding(Label.AutomationIdProperty, "."); - return new ViewCell() { View = label }; - }); + this.BindingContext = this; + var lv = new ListView() + { + Margin = new Thickness(20, 5, 5, 5) + }; - lv.SetBinding(ListView.ItemsSourceProperty, "Items"); + lv.ItemTemplate = new DataTemplate(() => + { + var label = new Label() { Text = "sassifrass" }; + label.SetBinding(Label.TextProperty, "."); + label.SetBinding(Label.AutomationIdProperty, "."); + return new ViewCell() { View = label }; + }); - this.Content = new ScrollView() - { - Content = new StackLayout() + lv.SetBinding(ListView.ItemsSourceProperty, "Items"); + + this.Content = new ScrollView() { - Children = + Content = new StackLayout() + { + Children = { new Label(){ Text = "If page is not frozen this test has passed" }, new StackLayout() { Orientation = StackOrientation.Horizontal, - Children = + Children = { - lv + lv } }, new Button(){ @@ -68,14 +75,15 @@ public FreezeMe() }) } } - } - }; + } + }; - this.Appearing += (s, e) => - { - this.Items = new List { 1, 2, 3 }; - this.OnPropertyChanged("Items"); - }; + this.Appearing += (s, e) => + { + this.Items = new List { 1, 2, 3 }; + this.OnPropertyChanged("Items"); + }; + } } } -} +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/TestCases.cs b/src/Controls/samples/Controls.Sample.UITests/TestCases.cs index a070a2c41d22..30e43adfeb0d 100644 --- a/src/Controls/samples/Controls.Sample.UITests/TestCases.cs +++ b/src/Controls/samples/Controls.Sample.UITests/TestCases.cs @@ -39,7 +39,6 @@ Action ActivatePageAndNavigate(IssueAttribute issueAttribute, Type type) return async () => { var page = ActivatePage(type); - TrackOnInsights(page); await Navigation.PushAsync(page); }; } @@ -49,38 +48,16 @@ Action ActivatePageAndNavigate(IssueAttribute issueAttribute, Type type) return async () => { var page = ActivatePage(type); - TrackOnInsights(page); await Navigation.PushModalAsync(page); }; } - if (issueAttribute.NavigationBehavior == NavigationBehavior.Default) - { - return async () => - { - var page = ActivatePage(type); - TrackOnInsights(page); - if (page is ContentPage /*|| page is CarouselPage*/) - { - await Navigation.PushAsync(page); - } - else if (page is Shell) - { - Application.Current.MainPage = page; - } - else - { - await Navigation.PushModalAsync(page); - } - }; - } - - if (issueAttribute.NavigationBehavior == NavigationBehavior.SetApplicationRoot) + if (issueAttribute.NavigationBehavior == NavigationBehavior.SetApplicationRoot || + issueAttribute.NavigationBehavior == NavigationBehavior.Default) { return () => { var page = ActivatePage(type); - TrackOnInsights(page); Application.Current.MainPage = page; }; } @@ -88,11 +65,6 @@ Action ActivatePageAndNavigate(IssueAttribute issueAttribute, Type type) return navigationAction; } - static void TrackOnInsights(Page page) - { - - } - Page ActivatePage(Type type) { var page = Activator.CreateInstance(type) as Page; @@ -182,7 +154,6 @@ public TestCaseScreen() }).ToList(); VerifyNoDuplicates(); - FilterIssues(); } @@ -212,6 +183,8 @@ public void FilterTracker(IssueTracker tracker) public bool TryToNavigateTo(string name) { var issue = _issues.SingleOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); + issue = _issues.SingleOrDefault(x => string.Equals(x.Description, name, StringComparison.OrdinalIgnoreCase)); + if (issue == null) return false; @@ -219,12 +192,11 @@ public bool TryToNavigateTo(string name) return true; } - public async void FilterIssues(string filter = null) + public void FilterIssues(string filter = null) { filter = filter?.Trim(); _filter = filter; - await Task.Delay(10); if (_filter != filter) { return; diff --git a/src/Controls/samples/Controls.Sample/Pages/PlatformSpecifics/iOS/iOSHideHomeIndicatorPage.xaml b/src/Controls/samples/Controls.Sample/Pages/PlatformSpecifics/iOS/iOSHideHomeIndicatorPage.xaml index ae76e6e58ba3..0d7e2af9c3b8 100644 --- a/src/Controls/samples/Controls.Sample/Pages/PlatformSpecifics/iOS/iOSHideHomeIndicatorPage.xaml +++ b/src/Controls/samples/Controls.Sample/Pages/PlatformSpecifics/iOS/iOSHideHomeIndicatorPage.xaml @@ -1,14 +1,14 @@ - + - -