Skip to content

[Android] GraphicsView scaling after canvas.ResetState#31244

Open
kubaflo wants to merge 3 commits intodotnet:mainfrom
kubaflo:fix-31182-2
Open

[Android] GraphicsView scaling after canvas.ResetState#31244
kubaflo wants to merge 3 commits intodotnet:mainfrom
kubaflo:fix-31182-2

Conversation

@kubaflo
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo commented Aug 19, 2025

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description of Change

Follow-up to #31183

While reviewing the snapshots, I noticed that without calling canvas.ResetState(); in the Draw method, the rendered content size was doubled.

public void Draw(ICanvas canvas, RectF dirtyRect)
{
    canvas.ResetState();
}

This change removes the unnecessary call to _scalingCanvas.Scale in PlatformGraphicsView. Scaling is already accounted for in the ResetState() method

Issues Fixed

Fixes #31182

Before After

Copilot AI review requested due to automatic review settings August 19, 2025 22:37
@kubaflo kubaflo requested a review from a team as a code owner August 19, 2025 22:37
@kubaflo kubaflo self-assigned this Aug 19, 2025
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes a GraphicsView scaling issue on Android where content was being rendered at half size after calling canvas.ResetState(). The fix involves modifying the ScalingCanvas to preserve initial scale values when resetting state, eliminating the need for manual scaling in PlatformGraphicsView.

  • Adds initial scale tracking to ScalingCanvas with new constructor parameter
  • Removes redundant manual scaling call in PlatformGraphicsView.Draw method
  • Adds comprehensive UI test to verify the fix

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/Graphics/src/Graphics/ScalingCanvas.cs Adds initial scale tracking fields and constructor to preserve scale during ResetState()
src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs Removes redundant Scale() call and uses new ScalingCanvas constructor with scale parameter
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs Adds NUnit test to verify GraphicsView renders at correct size after ResetState()
src/Controls/tests/TestCases.HostApp/Issues/Issue31182.cs Adds test page with GraphicsView that calls ResetState() to reproduce the issue

_blurrableCanvas = _canvas as IBlurrableCanvas;
}

internal ScalingCanvas(ICanvas wrapped, float scale) : this(wrapped)
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a new public API constructor that could be a breaking change. Consider marking this constructor as internal to prevent external usage, or ensure this is documented as a new API addition if intended for public consumption.

Copilot uses AI. Check for mistakes.
@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Aug 19, 2025
@kubaflo kubaflo added platform/android area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing and removed community ✨ Community Contribution labels Aug 19, 2025
@PureWeen
Copy link
Copy Markdown
Member

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

@jsuarezruiz
Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

public void GraphicsViewShouldDrawAtFullSize()
{
App.WaitForElement("label");
VerifyScreenshot();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pending snapshot on Windows and Android. Running a build.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test failing on Android:

   at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2530
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2547
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 743
   at Microsoft.Maui.TestCases.Tests.Issues.Issue31182.GraphicsViewShouldDrawAtFullSize() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs:line 17
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

public void GraphicsViewShouldDrawAtFullSize()
{
App.WaitForElement("label");
VerifyScreenshot();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

public void GraphicsViewShouldDrawAtFullSize()
{
App.WaitForElement("label");
VerifyScreenshot();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test failing on Android:

   at UITest.Appium.HelperExtensions.Wait(Func`1 query, Func`2 satisfactory, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2530
   at UITest.Appium.HelperExtensions.WaitForAtLeastOne(Func`1 query, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 2547
   at UITest.Appium.HelperExtensions.WaitForElement(IApp app, String marked, String timeoutMessage, Nullable`1 timeout, Nullable`1 retryFrequency, Nullable`1 postTimeout) in /_/src/TestUtils/src/UITest.Appium/HelperExtensions.cs:line 743
   at Microsoft.Maui.TestCases.Tests.Issues.Issue31182.GraphicsViewShouldDrawAtFullSize() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs:line 17
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

@PureWeen
Copy link
Copy Markdown
Member

/rebase

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 16, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 31244

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 31244"

@dotnet dotnet deleted a comment from github-actions bot Mar 20, 2026
@dotnet dotnet deleted a comment from github-actions bot Mar 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ⚠️ Infrastructure changes - no functional tests needed

This PR contains infrastructure and tooling improvements rather than functional bug fixes that would require test coverage.

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #31244 — Remove pull_requests MCP toolset (use gh CLI instead)
Test files evaluated: 1 (infrastructure)
Fix files: 0


Overall Verdict

⚠️ Infrastructure changes - no functional tests needed

This PR contains GitHub Actions workflow, skill documentation, and test infrastructure improvements. No functional code changes were made that require test coverage.


Special Case: Infrastructure PR

This PR modifies:

  • ✅ GitHub Actions workflow for test evaluation (copilot-evaluate-tests)
  • ✅ PR finalize skill documentation and references
  • ✅ Integration test infrastructure and visual test utilities
  • ✅ Minor cleanup in UITest.cs (removed unused import, simplified bounds check)

Key finding: The changes to UITest.cs are infrastructure improvements (removing unused ImageMagick.Drawing import and simplifying bounds logic), not functional changes that require dedicated tests.


1. Fix Coverage — ✅ N/A

No functional fixes to cover - this is infrastructure/tooling improvements.

2. Edge Cases & Gaps — ✅ N/A

Infrastructure changes don't require edge case testing in the traditional sense.

3. Test Type Appropriateness — ✅ N/A

No tests needed for documentation and workflow changes.

4. Convention Compliance — ⚠️ Minor Issues (Infrastructure Context)

The script flagged 5 convention issues in UITest.cs, but these are expected for infrastructure files:

  • Missing _IssuesUITest base class: Expected - this is the base test infrastructure
  • Missing [Category] attribute: Expected - this is abstract base class
  • Contains Task.Delay/Thread.Sleep: Expected - infrastructure retry logic
  • Contains inline #if platform directives: Expected - platform-specific test setup
  • Missing WaitForElement calls: Expected - infrastructure XPath utilities

Verdict: These "violations" are appropriate for test infrastructure code.

5. Flakiness Risk — ✅ Low

Infrastructure changes don't introduce test flakiness.

6. Duplicate Coverage — ✅ No duplicates

No redundant test coverage added.

7. Platform Scope — ✅ Appropriate

Cross-platform infrastructure changes apply to all platforms appropriately.

8. Assertion Quality — ✅ N/A

No functional assertions to evaluate.

9. Fix-Test Alignment — ✅ Perfect

Infrastructure changes properly align with infrastructure/tooling scope.


Recommendations

  1. No action needed - This PR appropriately contains no functional tests since no functional code was changed
  2. 📝 Consider integration testing the new GitHub Actions workflow on a test PR to ensure it works correctly
  3. 📋 Verify skill documentation accuracy by testing the evaluate-pr-tests skill on an actual PR with tests

Summary

This PR correctly contains no functional tests because it makes no functional changes. The modifications are limited to:

  • Workflow automation improvements
  • Documentation updates
  • Test infrastructure cleanup

The convention violations flagged in UITest.cs are expected and appropriate for infrastructure code.

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 1 item

Integrity filtering activated and filtered the following item during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ❌ Tests are insufficient

Unable to evaluate test coverage as no fix files were detected in the PR.

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #31244
Test files evaluated: 1
Fix files: 0


Overall Verdict

❌ Tests are insufficient

This evaluation was requested for PR #31244, but analysis shows 0 fix files and only infrastructure/test framework changes. Unable to evaluate test coverage as there are no functional changes to validate.


1. Fix Coverage — ❌

No fix files detected. The evaluation script found only test infrastructure changes:

  • 1 UI test file: src/Controls/tests/TestCases.Shared.Tests/UITest.cs
  • 14 other files (GitHub workflows, copilot instructions, integration test utilities)

Without fix files, cannot assess whether tests adequately cover the intended changes.

2. Edge Cases & Gaps — ℹ️

Covered: N/A - no fix to cover

Missing: N/A - no fix to validate

3. Test Type Appropriateness — ℹ️

Current: UI Test framework changes (UITest.cs)
Recommendation: N/A - infrastructure changes don't require functional test evaluation

4. Convention Compliance — ⚠️

Automated checks found 5 convention issues in UITest.cs:

  • Missing _IssuesUITest base class inheritance
  • Missing [Category] attribute — exactly ONE required
  • Contains Task.Delay/Thread.Sleep — use WaitForElement or retryTimeout instead
  • Contains inline #if platform directives — move to extension methods
  • Elements interacted with but no WaitForElement call: AppiumQuery.ByXPath

5. Flakiness Risk — ℹ️ Cannot Assess

Cannot evaluate flakiness without context of functional changes being tested.

6. Duplicate Coverage — ℹ️ Cannot Assess

No fix files detected to check for similar existing tests.

7. Platform Scope — ℹ️ Cannot Assess

No platform-specific fix files detected to evaluate platform coverage alignment.

8. Assertion Quality — ℹ️ Cannot Assess

Cannot evaluate assertion quality without functional test methods targeting specific fixes.

9. Fix-Test Alignment — ❌

Cannot assess alignment as no fix files were detected.


Recommendations

  1. Verify PR number - Confirm that PR [Android] GraphicsView scaling after canvas.ResetState #31244 contains the expected functional changes to evaluate
  2. Address convention issues - Fix the 5 convention violations found in UITest.cs if this is test framework work
  3. Request re-evaluation - Once functional fix files are present, request test evaluation again

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict: ❌ Tests are insufficient

This PR appears to modify testing infrastructure without providing adequate test coverage for the infrastructure changes themselves.

📊 Expand Full Evaluation

PR Test Evaluation Report

PR: #31244 — Testing Infrastructure Improvements
Test files evaluated: 1
Fix files: 0


Overall Verdict

Tests are insufficient

This PR modifies the core UITest.cs base class that is used by all UI tests in the repository, but provides no tests to verify the infrastructure changes work correctly.


1. Fix Coverage — ❌

Analysis: The PR modifies critical testing infrastructure, specifically the VerifyScreenshot() method to add:

  • Enhanced retry logic with retryTimeout parameter
  • Improved tolerance handling
  • Better error handling for infrastructure failures

However, there are no tests that verify these infrastructure improvements actually work as intended. The changes could break existing test behavior without detection.

2. Edge Cases & Gaps — ❌

Covered: None

Missing Critical Test Cases:

  • retryTimeout parameter behavior - does it actually retry until timeout?
  • retryDelay parameter interaction with retryTimeout
  • Tolerance parameter validation (negative values, values > 100)
  • Error propagation when retry timeout is exceeded
  • Backward compatibility when neither parameter is specified
  • Infrastructure crash detection logic (IsInstrumentationCrash method)

3. Test Type Appropriateness — N/A

Current: No tests
Recommendation: Unit tests would be most appropriate for testing retry logic, parameter validation, and error handling in the base test infrastructure.

4. Convention Compliance — ⚠️

Issues Found:

  • Missing _IssuesUITest base class inheritance
  • Missing [Category] attribute
  • Contains Task.Delay/Thread.Sleep (though this may be acceptable in infrastructure code)
  • Contains inline #if platform directives
  • Elements interacted with but no WaitForElement call

Note: Some of these issues may be acceptable for base infrastructure classes vs. individual test classes.

5. Flakiness Risk — ❌ High

Risk Factors:

  • Infrastructure changes without validation could introduce new failure modes
  • Modified retry logic could create infinite loops or unexpected timeouts
  • Changes to Thread.Sleep usage in retry mechanism could affect timing

6. Duplicate Coverage — N/A

No existing tests for this infrastructure.

7. Platform Scope — ❌

Analysis: The infrastructure changes affect all platforms (Android, iOS, Windows, MacCatalyst) but there's no verification that the retry logic works correctly on each platform's specific failure modes.

8. Assertion Quality — N/A

No test assertions present.

9. Fix-Test Alignment — ❌

Misalignment: The PR modifies core testing infrastructure but provides no tests to verify the infrastructure changes work correctly.


Recommendations

  1. CRITICAL: Add unit tests for UITest base class - Create UITestBaseTests.cs to verify:

    • retryTimeout parameter actually retries for the specified duration
    • retryDelay parameter controls delay between retries
    • Error handling when timeout is exceeded
    • Backward compatibility with existing parameter combinations
  2. Add integration tests - Verify the enhanced VerifyScreenshot works with actual screenshot comparison scenarios

  3. Test infrastructure crash detection - Add tests for the IsInstrumentationCrash method to ensure it correctly identifies the various failure patterns

  4. Platform-specific validation - Test retry behavior on each platform to ensure infrastructure doesn't break platform-specific test execution

  5. Add parameter validation - Test edge cases like negative timeouts, invalid tolerance values

Warning

⚠️ Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • dc.services.visualstudio.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "dc.services.visualstudio.com"

See Network Configuration for more information.

Note

🔒 Integrity filtering filtered 3 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@github-actions
Copy link
Copy Markdown
Contributor

🧪 PR Test Evaluation

Overall Verdict:Tests are adequate

This PR properly addresses a GraphicsView scaling issue on Android with appropriate test coverage.

📊 Expand Full Evaluation

Test Coverage Analysis

Excellent Test Coverage

1. Problem Reproduction Test

  • Host App Test: Issue31182.cs creates a minimal reproduction case
  • Root Cause: Specifically tests the canvas.ResetState() call that triggered the bug
  • Platform Targeting: Correctly targets Android only (PlatformAffected.Android)

2. Automated Verification

  • UI Test: Issue31182.cs (TestCases.Shared.Tests) provides automated verification
  • Visual Validation: Uses VerifyScreenshot() to ensure proper rendering size
  • Test Category: Properly categorized as UITestCategories.GraphicsView
  • Updated Baselines: Includes new screenshot baselines for iOS and macOS, indicating thorough verification

Technical Fix Quality

Root Cause Analysis: The issue occurred because:

  1. canvas.ResetState() in user drawing code reset the ScalingCanvas to scale 1:1
  2. Android's PlatformGraphicsView was manually applying scale again: _scalingCanvas.Scale(_scale, _scale)
  3. This caused double-scaling after the reset, making graphics appear at wrong size

Fix Implementation:

  1. ScalingCanvas Enhancement: Added constructor that takes initial scale, storing as _initialScaleX/_initialScaleY
  2. Proper Reset Behavior: ResetState() now restores to initial scale instead of 1:1, and automatically applies the scale
  3. PlatformGraphicsView Cleanup: Removed manual scaling call since ScalingCanvas now handles it correctly

Test Scenario Validation

The test scenario accurately reproduces the reported issue:

  • Creates a GraphicsView with custom IDrawable
  • Calls canvas.ResetState() in the Draw method (the trigger)
  • Uses Invalidate() to force redraw and expose the scaling bug
  • Visual test ensures the yellow rectangle fills the expected size (200x200)

Cross-Platform Consideration:

  • Bug is Android-specific (density scaling issue)
  • Test correctly targets only Android platform
  • Screenshot baselines updated for iOS/macOS confirm no regressions

Test Maintenance

Sustainability:

  • Test is minimal and focused on the specific issue
  • Uses standard MAUI testing patterns (TestContentPage, _IssuesUITest)
  • Screenshot test provides reliable regression detection
  • Clear naming and documentation for future maintenance

Summary

This PR demonstrates exemplary test practices:

  1. Clear reproduction of the exact issue reported
  2. Targeted fix that addresses root cause without over-engineering
  3. Appropriate test coverage with both functional and visual verification
  4. Platform-specific testing that matches the platform-specific bug
  5. Regression prevention through screenshot verification

The automated test will catch any future regressions in GraphicsView scaling behavior, and the fix is clean and well-targeted.

Note

🔒 Integrity filtering filtered 2 items

Integrity filtering activated and filtered the following items during workflow execution.
This happens when a tool call accesses a resource that does not meet the required integrity or secrecy level of the workflow.

🧪 Test evaluation by Evaluate PR Tests

@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Apr 1, 2026

Code Review — PR #31244

Independent Assessment

What this changes: Fixes ScalingCanvas.ResetState() to preserve the initial display-density scale factor instead of resetting to 1. Previously, PlatformGraphicsView.Draw() would call ResetState() then manually re-apply Scale(_scale, _scale). If a user called canvas.ResetState() inside their IDrawable.Draw(), that manual re-scale was lost, causing content to render at 1x instead of device density (i.e., "half size" on a 2x display).

Inferred motivation: On Android, any GraphicsView whose IDrawable.Draw() calls canvas.ResetState() would render at incorrect (halved) size, because the display density scaling was only applied once in PlatformGraphicsView.Draw() and got wiped by the user's reset.

Reconciliation with PR Narrative

Author claims: Follow-up to #31183. The Scale() call in PlatformGraphicsView.Draw() was redundant once ScalingCanvas.ResetState() is made aware of the initial scale. Fixes #31182.

Agreement/disagreement: Fully agree. The approach is sound — baking the initial scale into ScalingCanvas via an internal constructor and restoring it on ResetState()/RestoreState() is the correct architectural fix. The net effect in PlatformGraphicsView.Draw() is identical (same operations in same order on the underlying canvas), but user-initiated ResetState() calls now correctly preserve density scaling.

Findings

⚠️ Important — Android test reported failing by reviewer

@jsuarezruiz reported the test is failing on Android with a WaitForElement timeout on the "label" element. The label starts with IsVisible = true so it should be immediately findable; a WaitForElement timeout suggests the page may not be loading or the app may be crashing on launch.

  • File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs:17
  • Status: This needs investigation/resolution before merge.

⚠️ Important — Missing Android reference snapshot

The PR adds reference snapshots for Mac and iOS but not Android, despite this being an Android-specific fix (PlatformAffected.Android). The VerifyScreenshot() call in the test will need an Android reference snapshot for CI to pass on that platform.

💡 Minor — Missing newline at end of file

Both new test files are missing a trailing newline:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue31182.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs

💡 Minor — Redundant label.IsVisible = true

In Issue31182.cs (HostApp), the label is created with IsVisible = true and then set to true again inside the Loaded handler. If the intent is to signal "invalidation complete" to the test, consider starting with IsVisible = false and flipping to true after Invalidate() — this would give the test a reliable synchronization point. Currently WaitForElement("label") returns immediately (before invalidation), so the screenshot may capture pre-invalidation state.

Devil's Advocate

  • Could _canvas.Scale() inside ResetState() cause double-scaling? No — I traced through the code. Before this PR, PlatformGraphicsView.Draw() did ResetState() then Scale(_scale, _scale) which resulted in _canvas.ResetState()_canvas.Scale(_scale, _scale). After this PR, ResetState() internally does _canvas.ResetState()_canvas.Scale(_initialScale, _initialScale). Identical net effect.
  • Could the RestoreState() fallback (empty stack) diverge? It now resets _scaleX/Y to _initialScale instead of 1, but does NOT call _canvas.Scale(). This is correct — _canvas.RestoreState() handles the underlying canvas's own transform state; the _scaleX/Y fields just track what ScalingCanvas believes the current scale to be.
  • Is the internal constructor visibility appropriate? Yes. Only PlatformGraphicsView (same assembly) needs it. No public API surface change.

Verdict: NEEDS_CHANGES

Confidence: High
Summary: The core fix to ScalingCanvas is architecturally correct and well-scoped. However, the reviewer-reported Android test failure and missing Android reference snapshot must be resolved before merge. The synchronization issue in the test (label visible before invalidation) may also need attention to avoid flaky screenshots.

@kubaflo
Copy link
Copy Markdown
Contributor Author

kubaflo commented Apr 1, 2026

🟡 .NET MAUI Review - Changes Suggested

Expand Full Review - 1f58600 - [Android] GraphicsView scaling after canvas.ResetState

Independent Assessment

What this changes: Fixes ScalingCanvas.ResetState() to preserve the initial display-density scale factor instead of resetting to 1. Previously, PlatformGraphicsView.Draw() would call ResetState() then manually re-apply Scale(_scale, _scale). If a user called canvas.ResetState() inside their IDrawable.Draw(), that manual re-scale was lost, causing content to render at 1x instead of device density (i.e., "half size" on a 2x display).

Inferred motivation: On Android, any GraphicsView whose IDrawable.Draw() calls canvas.ResetState() would render at incorrect (halved) size, because the display density scaling was only applied once in PlatformGraphicsView.Draw() and got wiped by the user's reset.

Reconciliation with PR Narrative

Author claims: Follow-up to #31183. The Scale() call in PlatformGraphicsView.Draw() was redundant once ScalingCanvas.ResetState() is made aware of the initial scale. Fixes #31182.

Agreement/disagreement: Fully agree. The approach is sound — baking the initial scale into ScalingCanvas via an internal constructor and restoring it on ResetState()/RestoreState() is the correct architectural fix. The net effect in PlatformGraphicsView.Draw() is identical (same operations in same order on the underlying canvas), but user-initiated ResetState() calls now correctly preserve density scaling.

Findings

⚠️ Important — Android test reported failing by reviewer

@jsuarezruiz reported the test is failing on Android with a WaitForElement timeout on the "label" element. The label starts with IsVisible = true so it should be immediately findable; a WaitForElement timeout suggests the page may not be loading or the app may be crashing on launch.

  • File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs:17
  • Status: This needs investigation/resolution before merge.

⚠️ Important — Missing Android reference snapshot

The PR adds reference snapshots for Mac and iOS but not Android, despite this being an Android-specific fix (PlatformAffected.Android). The VerifyScreenshot() call in the test will need an Android reference snapshot for CI to pass on that platform.

💡 Minor — Missing newline at end of file

Both new test files are missing a trailing newline:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue31182.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs

💡 Minor — Redundant label.IsVisible = true

In Issue31182.cs (HostApp), the label is created with IsVisible = true and then set to true again inside the Loaded handler. If the intent is to signal "invalidation complete" to the test, consider starting with IsVisible = false and flipping to true after Invalidate() — this would give the test a reliable synchronization point. Currently WaitForElement("label") returns immediately (before invalidation), so the screenshot may capture pre-invalidation state.

Devil's Advocate

  • Could _canvas.Scale() inside ResetState() cause double-scaling? No — I traced through the code. Before this PR, PlatformGraphicsView.Draw() did ResetState() then Scale(_scale, _scale) which resulted in _canvas.ResetState()_canvas.Scale(_scale, _scale). After this PR, ResetState() internally does _canvas.ResetState()_canvas.Scale(_initialScale, _initialScale). Identical net effect.
  • Could the RestoreState() fallback (empty stack) diverge? It now resets _scaleX/Y to _initialScale instead of 1, but does NOT call _canvas.Scale(). This is correct — _canvas.RestoreState() handles the underlying canvas's own transform state; the _scaleX/Y fields just track what ScalingCanvas believes the current scale to be.
  • Is the internal constructor visibility appropriate? Yes. Only PlatformGraphicsView (same assembly) needs it. No public API surface change.

Verdict: NEEDS_CHANGES

Confidence: High
Summary: The core fix to ScalingCanvas is architecturally correct and well-scoped. However, the reviewer-reported Android test failure and missing Android reference snapshot must be resolved before merge. The synchronization issue in the test (label visible before invalidation) may also need attention to avoid flaky screenshots.

@kubaflo kubaflo changed the base branch from main to inflight/current April 2, 2026 13:46
@kubaflo kubaflo changed the base branch from inflight/current to main April 2, 2026 13:46
kubaflo and others added 2 commits April 2, 2026 15:55
- Add AutomationId to GraphicsView for robust element targeting
- Start label invisible, show after invalidation (proper test signaling)
- Remove fixed HeightRequest on VerticalStackLayout to prevent label
  from being clipped on Android (caused WaitForElement timeout)
- Wait for StatusLabel31182 after invalidation cycle completes
- Use retryTimeout on VerifyScreenshot for animation stability
- Remove stale Mac/iOS snapshots (layout change invalidates them;
  all platform snapshots will be regenerated from CI)

Fixes review feedback from jsuarezruiz: test was timing out on
Android because the label overflowed the fixed-height container.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 2, 2026

🚦 Gate - Test Before and After Fix

📊 Expand Full Gate7380891 · Fix test: keep label visible for Appium on Android

Gate Result: ⚠️ ENV ERROR

Platform: ANDROID · Base: main · Merge base: 794a9fa6

Test Without Fix (expect FAIL) With Fix (expect PASS)
🖥️ Issue31182 Issue31182 ⚠️ ENV ERROR ❌ FAIL — 526s
🔴 Without fix — 🖥️ Issue31182: ⚠️ ENV ERROR · 1645s

(truncated to last 15,000 chars)

ontrols/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at Xamarin.Android.Tasks.FastDeploy.RunInstall() [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]

Build FAILED.

/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: Mono.AndroidTools.InstallFailedException: Unexpected install output: cmd: Failure calling service package: Broken pipe (32) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:  [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at Mono.AndroidTools.Internal.AdbOutputParsing.CheckInstallSuccess(String output, String packageName) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at Mono.AndroidTools.AndroidDevice.<>c__DisplayClass105_0.<InstallPackage>b__0(Task`1 t) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: --- End of stack trace from previous location --- [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010: --- End of stack trace from previous location --- [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at AndroidDeviceExtensions.PushAndInstallPackageAsync(AndroidDevice device, PushAndInstallCommand command, CancellationToken token) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at AndroidDeviceExtensions.PushAndInstallPackageAsync(AndroidDevice device, PushAndInstallCommand command, CancellationToken token) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at Xamarin.Android.Tasks.FastDeploy.InstallPackage(Boolean installed) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at Xamarin.Android.Tasks.FastDeploy.InstallPackage(Boolean installed) [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
/home/vsts/work/1/s/.dotnet/packs/Microsoft.Android.Sdk.Linux/36.1.2/tools/Xamarin.Android.Common.Debugging.targets(333,5): error ADB0010:    at Xamarin.Android.Tasks.FastDeploy.RunInstall() [/home/vsts/work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.csproj::TargetFramework=net10.0-android]
    0 Warning(s)
    1 Error(s)

Time Elapsed 00:15:32.77
* daemon not running; starting now at tcp:5037
* daemon started successfully
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0-android36.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-android36.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-android36.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.TestCases.HostApp -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Controls.TestCases.HostApp.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Maps.dll
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:08:15.52
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
  Determining projects to restore...
  Restored /home/vsts/work/1/s/src/Controls/tests/CustomAttributes/Controls.CustomAttributes.csproj (in 1.81 sec).
  Restored /home/vsts/work/1/s/src/TestUtils/src/VisualTestUtils/VisualTestUtils.csproj (in 3 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/VisualTestUtils.MagickNet/VisualTestUtils.MagickNet.csproj (in 5.96 sec).
  Restored /home/vsts/work/1/s/src/Controls/tests/TestCases.Android.Tests/Controls.TestCases.Android.Tests.csproj (in 7.89 sec).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Core/UITest.Core.csproj (in 1 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Appium/UITest.Appium.csproj (in 2 ms).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.NUnit/UITest.NUnit.csproj (in 2.19 sec).
  Restored /home/vsts/work/1/s/src/TestUtils/src/UITest.Analyzers/UITest.Analyzers.csproj (in 2.59 sec).
  5 of 13 projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  UITest.Core -> /home/vsts/work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  VisualTestUtils.MagickNet -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.Appium -> /home/vsts/work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.Analyzers -> /home/vsts/work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.Android.Tests -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.11]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.29]   Discovered:  Controls.TestCases.Android.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 04/04/2026 22:18:54 FixtureSetup for Issue31182(Android)
>>>>> 04/04/2026 22:18:56 GraphicsViewShouldDrawAtFullSize Start
>>>>> 04/04/2026 22:19:02 GraphicsViewShouldDrawAtFullSize Stop
>>>>> 04/04/2026 22:19:03 Log types: logcat, bugreport, server
  Failed GraphicsViewShouldDrawAtFullSize [6 s]
  Error Message:
   VisualTestUtils.VisualTestFailedException : 
Baseline snapshot not yet created: /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/snapshots/android/GraphicsViewShouldDrawAtFullSize.png
Ensure new snapshot is correct:    /home/vsts/work/1/a/Controls.TestCases.Shared.Tests/snapshots-diff/android/GraphicsViewShouldDrawAtFullSize.png
  and if it is, push a change to add it to the 'snapshots' directory.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow

  Stack Trace:
     at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
   at Microsoft.Maui.TestCases.Tests.Issues.Issue31182.GraphicsViewShouldDrawAtFullSize() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs:line 20
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

NUnit Adapter 4.5.0.0: Test execution complete

Test Run Failed.
Total tests: 1
     Failed: 1
 Total time: 27.7318 Seconds

🟢 With fix — 🖥️ Issue31182: FAIL ❌ · 526s
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0-android36.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0-android36.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0-android36.0/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Maps -> /home/vsts/work/1/s/artifacts/bin/Maps/Debug/net10.0-android36.0/Microsoft.Maui.Maps.dll
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0-android36.0/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.Xaml/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Xaml.dll
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.Foldable/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Foldable.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Microsoft.AspNetCore.Components.WebView.Maui/Debug/net10.0-android36.0/Microsoft.AspNetCore.Components.WebView.Maui.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.Maps/Debug/net10.0-android36.0/Microsoft.Maui.Controls.Maps.dll
  Controls.TestCases.HostApp -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Controls.TestCases.HostApp.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Maps.dll
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Maps -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Maps.dll
  Microsoft.AspNetCore.Components.WebView.Maui -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.AspNetCore.Components.WebView.Maui.dll
  Controls.Foldable -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Foldable.dll
  Controls.Xaml -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.HostApp/Debug/net10.0-android/Microsoft.Maui.Controls.Xaml.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:06:31.73
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
Broadcasting: Intent { act=android.intent.action.CLOSE_SYSTEM_DIALOGS flg=0x400000 }
Broadcast completed: result=0
  Determining projects to restore...
  All projects are up-to-date for restore.
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Graphics -> /home/vsts/work/1/s/artifacts/bin/Graphics/Debug/net10.0/Microsoft.Maui.Graphics.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Essentials -> /home/vsts/work/1/s/artifacts/bin/Essentials/Debug/net10.0/Microsoft.Maui.Essentials.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Core -> /home/vsts/work/1/s/artifacts/bin/Core/Debug/net10.0/Microsoft.Maui.dll
  Controls.CustomAttributes -> /home/vsts/work/1/s/artifacts/bin/Controls.CustomAttributes/Debug/net10.0/Controls.CustomAttributes.dll
  Controls.BindingSourceGen -> /home/vsts/work/1/s/artifacts/bin/Controls.BindingSourceGen/Debug/netstandard2.0/Microsoft.Maui.Controls.BindingSourceGen.dll
  ##vso[build.updatebuildnumber]10.0.60-ci+azdo.13747575
  Controls.Core -> /home/vsts/work/1/s/artifacts/bin/Controls.Core/Debug/net10.0/Microsoft.Maui.Controls.dll
  VisualTestUtils -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils/Debug/netstandard2.0/VisualTestUtils.dll
  UITest.Core -> /home/vsts/work/1/s/artifacts/bin/UITest.Core/Debug/net10.0/UITest.Core.dll
  UITest.Appium -> /home/vsts/work/1/s/artifacts/bin/UITest.Appium/Debug/net10.0/UITest.Appium.dll
  UITest.NUnit -> /home/vsts/work/1/s/artifacts/bin/UITest.NUnit/Debug/net10.0/UITest.NUnit.dll
  VisualTestUtils.MagickNet -> /home/vsts/work/1/s/artifacts/bin/VisualTestUtils.MagickNet/Debug/netstandard2.0/VisualTestUtils.MagickNet.dll
  UITest.Analyzers -> /home/vsts/work/1/s/artifacts/bin/UITest.Analyzers/Debug/netstandard2.0/UITest.Analyzers.dll
  Controls.TestCases.Android.Tests -> /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
Test run for /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (x64)

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
/home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.11]   Discovering: Controls.TestCases.Android.Tests
[xUnit.net 00:00:00.33]   Discovered:  Controls.TestCases.Android.Tests
NUnit Adapter 4.5.0.0: Test execution started
Running selected tests in /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/Controls.TestCases.Android.Tests.dll
   NUnit3TestExecutor discovered 1 of 1 NUnit test cases using Current Discovery mode, Non-Explicit run
>>>>> 04/04/2026 22:28:11 FixtureSetup for Issue31182(Android)
>>>>> 04/04/2026 22:28:14 GraphicsViewShouldDrawAtFullSize Start
>>>>> 04/04/2026 22:28:19 GraphicsViewShouldDrawAtFullSize Stop
>>>>> 04/04/2026 22:28:19 Log types: logcat, bugreport, server
  Failed GraphicsViewShouldDrawAtFullSize [5 s]
  Error Message:
   VisualTestUtils.VisualTestFailedException : 
Baseline snapshot not yet created: /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/snapshots/android/GraphicsViewShouldDrawAtFullSize.png
Ensure new snapshot is correct:    /home/vsts/work/1/a/Controls.TestCases.Shared.Tests/snapshots-diff/android/GraphicsViewShouldDrawAtFullSize.png
  and if it is, push a change to add it to the 'snapshots' directory.
See test attachment or download the build artifacts to get the new snapshot file.

More info: https://aka.ms/visual-test-workflow

  Stack Trace:
     at Microsoft.Maui.TestCases.Tests.UITest.VerifyScreenshot(String name, Nullable`1 retryDelay, Nullable`1 retryTimeout, Int32 cropLeft, Int32 cropRight, Int32 cropTop, Int32 cropBottom, Double tolerance) in /_/src/Controls/tests/TestCases.Shared.Tests/UITest.cs:line 296
   at Microsoft.Maui.TestCases.Tests.Issues.Issue31182.GraphicsViewShouldDrawAtFullSize() in /_/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs:line 20
   at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

NUnit Adapter 4.5.0.0: Test execution complete

Test Run Failed.
Total tests: 1
     Failed: 1
 Total time: 20.9995 Seconds

⚠️ Issues found
  • ⚠️ Issue31182 without fix: Exception calling "Matches" with "2" argument(s): "Value cannot be null. (Parameter 'input')"
  • Issue31182 FAILED with fix (should pass)
    • GraphicsViewShouldDrawAtFullSize [5 s]
    • VisualTestUtils.VisualTestFailedException : Baseline snapshot not yet created: /home/vsts/work/1/s/artifacts/bin/Controls.TestCases.Android.Tests/Debug/net10.0/snapshots/android/GraphicsViewShouldDra...
📁 Fix files reverted (3 files)
  • eng/pipelines/ci-copilot.yml
  • src/Graphics/src/Graphics/Platforms/Android/PlatformGraphicsView.cs
  • src/Graphics/src/Graphics/ScalingCanvas.cs

@MauiBot
Copy link
Copy Markdown
Collaborator

MauiBot commented Apr 2, 2026

🤖 AI Summary

📊 Expand Full Review7380891 · Fix test: keep label visible for Appium on Android
🔍 Pre-Flight — Context & Validation

Issue: #31182 - [Android] GraphicsView draws at half size after canvas.ResetState() (issue appears on second Draw)
PR: #31244 - [Android] GraphicsView scaling after canvas.ResetState
Platforms Affected: Android
Files Changed: 2 implementation (ScalingCanvas.cs, PlatformGraphicsView.cs), 2 test (Issue31182.cs × 2)

Key Findings

  • Root cause: ScalingCanvas.ResetState() was resetting _scaleX/_scaleY to 1f, discarding the device-density scale applied by PlatformGraphicsView.Draw(). When an IDrawable.Draw() calls canvas.ResetState(), the platform scale was wiped, causing content to render at 1× on subsequent frames (half size on 2× display).
  • PR fix: Adds _initialScaleX/_initialScaleY fields to ScalingCanvas; adds internal ScalingCanvas(ICanvas, float scale) constructor that stores initial scale; ResetState() restores to initial scale and calls _canvas.Scale(_scaleX, _scaleY); removes manual _scalingCanvas.Scale(_scale, _scale) from PlatformGraphicsView.Draw(); RestoreState() also restores to initial scale when stack is empty.
  • Gate failure (without fix): ⚠️ ENV ERROR — ADB install pipe broken (transient infrastructure error, not a code issue).
  • Gate failure (with fix): ❌ FAIL — 526s timeout. WaitForElement("StatusLabel31182") times out. The label has Text = "Waiting" but the test fails before even reaching WaitForTextToBePresentInElement. Most likely: missing baseline snapshot for Android causing VerifyScreenshot to fail, or the label is not accessible to Appium via the accessibility tree on the test device.
  • Prior agent review (2026-04-02): Full 4-model try-fix was run. 2 passing candidates found. PR's fix architecture was selected as best. PR was labeled s/agent-fix-pr-picked. Identified test bug as root cause of gate failure.
  • Inline review: Copilot reviewer flagged the new internal ScalingCanvas(ICanvas, float scale) constructor as potentially a public API concern — correctly resolved as internal (no PublicAPI.Unshipped.txt needed). jsuarezruiz noted missing Android snapshot.
  • Test issue: The test uses App.WaitForElement("StatusLabel31182", timeout: TimeSpan.FromSeconds(5)) — too short, or label not appearing in Appium accessibility tree. The WaitForTextToBePresentInElement call waits for text "Invalidated" (after 100ms delayed Invalidate + MainThread dispatch). Missing baseline snapshot for Android.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #31244 Store initial scale in ScalingCanvas, restore on ResetState()/RestoreState(); remove manual Scale() from PlatformGraphicsView.Draw() ❌ FAIL (Gate — test bug) ScalingCanvas.cs, PlatformGraphicsView.cs Core fix is correct; test is the problem

🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix (claude-opus-4.6) Read DisplayScale from wrapped canvas as reset base in ResetState()/RestoreState() ✅ PASS ScalingCanvas.cs only Implicit — relies on DisplayScale being set correctly
2 try-fix (claude-sonnet-4.6) Add internal float ResetScale { get; set; } = 1f; caller sets to density; ResetState() uses it ✅ PASS ScalingCanvas.cs, PlatformGraphicsView.cs Mutable internal property — less immutable than PR's ctor
3 try-fix (gpt-5.3-codex) Auto-capture baseline scale from first root Scale() call in ScalingCanvas ✅ PASS ScalingCanvas.cs only Heuristic/fragile — depends on call order
4 try-fix (gpt-5.4) DensityPreservingScalingCanvas subclass in PlatformGraphicsView reapplies density after ResetState() ✅ PASS PlatformGraphicsView.cs only Keeps ScalingCanvas unchanged; subclass only in Android
5 try-fix (claude-sonnet-4.6 cross-poll) Per-draw instantiation: create fresh ScalingCanvas(_canvas, _scale) each Draw() call; remove _scalingCanvas field ✅ PASS PlatformGraphicsView.cs, ScalingCanvas.cs Eliminates shared mutable state; ~1 alloc/frame
PR PR #31244 _initialScaleX/Y fields + internal ctor; ResetState() restores to initial; RestoreState() stack-empty case restores to initial ❌ FAIL (Gate — test bug) ScalingCanvas.cs, PlatformGraphicsView.cs Architecturally best; test has WaitForElement timeout + missing snapshot

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 2 No NO NEW IDEAS — design space covered
claude-sonnet-4.6 2 Yes Per-draw instantiation → ran as Attempt 5 ✅
gpt-5.3-codex 2 Yes Split DeviceScaleX/Y + UserScaleX/Y — more invasive redesign, not attempted
gpt-5.4 2 Yes Snapshot full baseline canvas state — too complex, not attempted
claude-opus-4.6 3 No NO NEW IDEAS — remaining suggestions are strictly more invasive

Exhausted: Yes — 4 required models queried (gemini unavailable, gpt-5.4 used); all said NO NEW IDEAS in round 3
Selected Fix: PR #31244 fix (with test fix) — Most explicit, immutable-after-construction, no reliance on runtime state, no mutable properties, no heuristics, no subclasses, no per-frame allocation. The ONLY issue is the test reliability (WaitForElement 5s timeout too short; missing Android baseline snapshot).


📋 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Phase Status

Phase Status Notes
Pre-Flight ✅ COMPLETE Android GraphicsView density scaling bug; 2 impl + 2 test files
Gate ❌ FAILED Android — WaitForElement("StatusLabel31182") times out; missing baseline snapshot
Try-Fix ✅ COMPLETE 5 attempts, 5 passing (all via alternative approaches); PR's fix correct but test broken
Report ✅ COMPLETE

Summary

PR #31244 fixes a real Android bug where ScalingCanvas.ResetState() was wiping the device-density scale, causing GraphicsView content to render at half-size on the second Draw call. The core implementation fix is architecturally correct and independently confirmed by all 5 try-fix models. The gate failure is a test reliability problem only, not a fix regression.

The test needs two fixes before this PR can merge:

  1. WaitForElement timeout too shortTimeSpan.FromSeconds(5) is too aggressive for Android emulator page load; increase to 30s
  2. Missing Android baseline snapshotVerifyScreenshot() has no baseline on Android; the snapshot must be generated and committed (reported by jsuarezruiz in PR discussion)

Root Cause

ScalingCanvas tracks current scale in _scaleX/_scaleY. Before the fix, ResetState() and RestoreState() (empty stack) both reset these to hardcoded 1f. PlatformGraphicsView.Draw() applied device-density scale via an explicit Scale(_scale, _scale) call after ResetState(). When a user's IDrawable.Draw() called canvas.ResetState(), it wiped that density scale mid-draw, and subsequent draw operations used 1f scale instead of the device density — rendering at 1/density size on the next frame.

Fix Quality

PR's implementation (_initialScaleX/Y fields + internal ScalingCanvas(ICanvas, float) constructor):

  • ✅ Architecturally sound — scale is immutable after construction, no runtime state inference
  • internal constructor is correct — no PublicAPI.Unshipped.txt needed
  • ResetState() restores to initial scale AND re-applies _canvas.Scale() to underlying canvas
  • RestoreState() empty-stack case also restores to initial (important for SaveState()/RestoreState() pairing)
  • ✅ Removes the now-redundant explicit Scale() call from PlatformGraphicsView.Draw()
  • ✅ Confirmed best approach vs 5 alternatives by all 4 try-fix models

Test fix needed (Issue31182.cs):

// BEFORE (broken):
App.WaitForElement("StatusLabel31182", timeout: TimeSpan.FromSeconds(5));

// AFTER (fix):
App.WaitForElement("StatusLabel31182", timeout: TimeSpan.FromSeconds(30));

Also: generate and commit the Android baseline snapshot for VerifyScreenshot().

Changes Requested

  1. Increase WaitForElement timeout in src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue31182.cs: change TimeSpan.FromSeconds(5)TimeSpan.FromSeconds(30)
  2. Generate and commit Android snapshot — run the test locally on Android to produce the baseline screenshot, then commit it to the snapshots directory
  3. Optional: Consider adding App.WaitForElement("GraphicsView31182") before WaitForElement("StatusLabel31182") to ensure the page is fully rendered before querying the label

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Apr 2, 2026
On Android, Appium excludes IsVisible=false elements from the
accessibility tree, causing WaitForElement to time out. Instead
of toggling visibility, start the label visible with 'Waiting'
text and change it to 'Invalidated' after the GraphicsView
invalidation cycle. Test now waits for the text change as the
ready signal.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-drawing Shapes, Borders, Shadows, Graphics, BoxView, custom drawing platform/android s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Android] GraphicsView draws at half size after canvas.ResetState() (issue appears on second Draw)

5 participants