Skip to content

Add device tests for CommunityToolkit.Maui, Apply Fixes for Bugs Found - #3344

Draft
TheCodeTraveler wants to merge 106 commits into
mainfrom
Add-device-tests-and-ui-tests
Draft

TheCodeTraveler wants to merge 106 commits into
mainfrom
Add-device-tests-and-ui-tests

Conversation

@TheCodeTraveler

@TheCodeTraveler TheCodeTraveler commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

This is a work in progress built upon @ne0rrmatrix’s work in #3277. It includes additional tests and additional bug fixes that the test discovered:

Partially Fixed (will be finished in a future PR): #3317, #2894, #2691, #3063, #2952, #1987, #2645, #3220, #3248, #2602

Partially Fixed (already closed, will be finished with #3317): #2383, #2950

Copilot AI balanced review requested due to automatic review settings October 10, 2026 22:02
@TheCodeTraveler
TheCodeTraveler marked this pull request as draft October 10, 2026 22:04

This comment was marked as outdated.

@TheCodeTraveler TheCodeTraveler changed the title Add device tests for CommunityToolkit.Maui Add device tests for CommunityToolkit.Maui, Apply Fixes for Bugs Found Oct 10, 2026
TheCodeTraveler added a commit that referenced this pull request Oct 11, 2026
…evice tests

- CameraProvider (Windows): the camera refresh skips a camera that InitializeCameraForCameraView could not open, instead
  of reading the VideoDeviceController of a MediaCapture that never initialized (its getter throws, so one such camera
  made the whole refresh fail). New Windows device test:
  CameraProviderWindowsTests.RefreshWhileAnotherCaptureHoldsACameraListsExactlyTheCamerasThatCanBeOpened.
  (Copilot review on #3344)
- Device tests: the AttachAndRun tests of the Maui, MediaElement and Camera apps wait for the Loaded and Unloaded events
  they count. OnLoadedAsync returns once the view is loaded, which can come before MAUI raises Loaded on a slow
  emulator (Android API 28 in CI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TheCodeTraveler
TheCodeTraveler requested a balanced review from Copilot October 11, 2026 02:56

This comment was marked as outdated.

@TheCodeTraveler

Copy link
Copy Markdown
Collaborator Author

We're now up to 89.5% Line Coverage and 82% Branch coverage across all unit tests and device tests. That's pretty great!

Code Coverage Report:
https://github.com/CommunityToolkit/Maui/actions/runs/38106699644/attempts/1#summary-114378095598

image

This comment was marked as outdated.

This comment was marked as outdated.

This comment was marked as outdated.

@TheCodeTraveler TheCodeTraveler added the breaking change This label is used for PRs that include a breaking change label Oct 11, 2026
ne0rrmatrix added a commit to ne0rrmatrix/EbpubReaderV2 that referenced this pull request Oct 14, 2026
- Unit tests for CalibreCoverUrl, OpdsEntryStagingCache, EpubPathUtilities,
  EpubArchive and PositionSyncService.
- tests/DisplayBook.DeviceTests: a MAUI app that runs xUnit on the device
  (DeviceRunners), covering code that calls MAUI APIs: BookStorageService,
  BookDatabase, OpdsServerRepository, OpdsCatalogCache, FirebaseAuthService
  (SecureStorage), GoogleBooksOptions (Preferences) and the value converters.
  Modelled on CommunityToolkit/Maui#3344.
- tools/coverage.ps1: unit tests + Windows device tests under dotnet-coverage,
  merged into artifacts/coverage/coverage.xml (Visual Studio XML).
- .github/workflows/build.yml: build, tests and coverage between the
  SonarQube Cloud scanner's begin/end.
- Android signing: the keystore and its passwords leave the repository. They
  now come from Secrets.local.props locally and from repository secrets in CI;
  without them Android builds sign with the debug key.
…the bugs they found

Platform code that the net10.0 unit tests cannot reach is now covered by
device tests that run inside real apps on Android, iOS, Mac Catalyst and
Windows. There is one device-test app per shipped feature package
(CommunityToolkit.Maui.DeviceTests for CommunityToolkit.Maui, .Core and
.Maps; CommunityToolkit.Maui.MediaElement.DeviceTests;
CommunityToolkit.Maui.Camera.DeviceTests), built like dotnet/maui's device
tests: xUnit, a vendored test runner and XHarness. eng/devices/android.ps1,
ios.ps1, catalyst.ps1 and windows.ps1 build and run them locally and in CI;
eng/devices/README.md and src/CommunityToolkit.Maui.DeviceTests/README.md
describe how to run and write them.

The Build workflow (dotnet-build.yml) calls the reusable Device Tests
workflow (device-tests.yml), which runs every app on Android API 28 and 36,
the iOS simulator, Mac Catalyst and Windows (packaged and unpackaged). Its
Code Coverage job merges the unit-test and device-test coverage into one
report. Tool versions shared by the workflows live in
.github/workflows/shared.env.

The tests found bugs in the libraries, which this change fixes: #3307,
#2827, #1818, #1826, #2358, #1845, #3091, #2320, #2153, #1879, #2078,
#2570, #3079, #3113, #2900, #3212, #2519, #3171, #3050, #2322 and #2208,
and in part #3317, #2894, #3081, #2538, #2691, #3063, #2952, #1987, #2645,
#3220, #3248, #3066 and #3247.

Rebased onto main as one commit. Conflicts:
- StatusBar.android.cs: keeps #3189's window flags (the decor never fits
  the system windows, API < 30 also clears LayoutNoLimits, nothing depends
  on transparency) with this change's FindChildWithTag lookups. This
  change's alpha-0 check is no longer needed. The Android StatusBar device
  tests now expect #3189's flags.
- Popup.shared.cs, PopupPage.shared.cs: keeps #3345's
  DefaultPopupSettings.BackgroundColorDark and OnInitializing, and this
  change's Style support. A value or binding set on the Popup, like the
  AppThemeBinding that #3345 set in the constructor, takes precedence over
  every Style (7 of 8 PopupStyleTests fail with it), so Popup.BackgroundColor
  stays null by default and the popup's Border shows
  DefaultPopupSettings.BackgroundColor, or BackgroundColorDark in the dark
  theme, following theme changes. #3345's two default-color tests now
  expect Popup.BackgroundColor to be null and check the Border.
Also: actions/upload-artifact and download-artifact pinned to main's 7.0.2
and 8.0.2 in the device-test and coverage jobs; the empty property pattern
(is { }), which CONTRIBUTING.md now asks to avoid, replaced by type
patterns; the comments of DialogFragmentService and its tests no longer
say that StatusBar.SetColor treats colors without alpha as transparent; and
the Sample's popups that set black text but no background color
(UpdatingPopup, CollectionViewPopup, ComplexPopup, CsharpBindingPopup and
the Popup Sizing Issues popup) now show white text in the dark theme, on
#3345's black default background (DefaultPopupSettings.BackgroundColorDark).

Co-authored-by: James Crutchley <ne0rmatrix@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TheCodeTraveler
TheCodeTraveler force-pushed the Add-device-tests-and-ui-tests branch from 2cda4d3 to 3f57379 Compare October 16, 2026 04:57
…and agents

CONTRIBUTING.md gains a Comments section: names say what the code does;
comments only explain why (a non-intuitive decision, a platform quirk or
workaround, an ordering constraint, code that looks out of scope).
.github/copilot-instructions.md and .github/instructions/codestyle.instructions.md
carry the same rule and the existing CONTRIBUTING.md rule against the empty
property pattern `is { }`, and the range-indexer example no longer teaches
comments that restate the code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This comment was marked as outdated.

On .NET 11, StatusBar.SetStyle and StatusBarBehavior.StatusBarStyle only set
Window.StatusBarTheme, which UIKit ignores when Info.plist sets
UIViewControllerBasedStatusBarAppearance to false: the setup that the
toolkit documents for StatusBarBehavior. The style stopped changing, while
the same app with the .NET 10 code (UIApplication.SetStatusBarStyle) shows
it. .NET 11 rc.1 builds against the iOS 26.5 SDK, and UIKit applies
UIApplication.SetStatusBarStyle with that key in every app built with an iOS
SDK before 27.0, with or without the UIScene life cycle; only apps built with
the iOS 27 SDK ignore it.

With the key set to false, the .NET 11 code now also sets the style of the
whole app with UIApplication.SetStatusBarStyle, as .NET 10 does, and writes
its "remove the key" trace message only when UIKit ignores that call too.
The remarks and comments no longer say that every .NET 11 app is scene-based
or that scene-based apps ignore UIApplication.SetStatusBarStyle: the iOS SDK
the app is built with decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TheCodeTraveler and others added 15 commits October 16, 2026 13:55
…orm to 100%" typo

The Button, Multiple Button, Toggle Size, Cannot Be Dismissed, Return Result and Opened Event Simple popups have a fixed light background (White, or LightCoral for the Button popup), but their description label used a "Content" style without a TextColor. Because the label has an explicit Style, the Sample's implicit Label style does not apply to it, so the label used the platform's default text colour, which is white in dark theme (the implicit style would have made it white too): the text vanished on iOS, Android and Windows. Each Content style now sets TextColor #000, like the Title style next to it.

The description read "will confirm to 100%" in the Multiple Button, Toggle Size and Cannot Be Dismissed popups and in XamlBindingPopupViewModel; it now reads "conform", as in ButtonPopup.

The Cannot Be Dismissed popup chose its description with OnPlatform for Android, iOS and WinUI only. OnPlatform has no iOS fallback for Mac Catalyst, so the label was empty there, and the WinUI text described the old Flyout-based popup ("Windows Flyouts do not have support for toggling light dismiss mode"), while the PopupPage-based popup keeps CanBeDismissedByTappingOutsideOfPopup = false on Windows too. The label now shows the one corrected description on every platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… instead of a misleading error (S-FILESAVER-CANCEL)

After the fix for a canceled prompt (the previous commit), "Save using FileSaver from DI Container" still passed an empty file name to SaveAsync when the person pressed OK without typing one. On iOS the page then reported "File is not saved, Access to the path '<app>/tmp/<guid>' is denied." (RAN on the iOS 27 simulator): the library writes the file to a new temporary folder before the save UI opens, and an empty name names that folder. Mac Catalyst runs the same code (INFERRED). Android 36 opened the save UI with an empty name (RAN).

An empty or blank name now shows the toast "File is not saved, the file name is empty" on every platform, and nothing is saved. A canceled prompt still ends the command without a toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the package before the save UI opens (S-FILESAVER-DOWNLOAD)

SaveFileInstance started the nuget.org download with HttpClient.GetStreamAsync before its try block and handed the open network stream to SaveAsync, which on Android reads it only after the person has chosen where to save. On Android (RAN on API 36 and 28 before this change):
- offline, the WebException of the download ("Unable to resolve host "www.nuget.org"") escaped the command and ended the app;
- when choosing the location took about 6 seconds, the saved file was incomplete (665,031 of 1,167,815 bytes), and the app then ended with a NetworkOnMainThreadException;
- after a canceled save, the app ended with a NetworkOnMainThreadException: `await using` disposed the unread response stream on the main thread, and Android's HTTP stack reads the rest of a response when its stream is closed.

The command now downloads the whole package with HttpClient.GetByteArrayAsync inside the try block and saves a MemoryStream of it. HttpClient reads the response to its end and disposes it itself, a failed download shows "File is not saved, <reason>" like a failed save, and the time spent in the save UI no longer matters. The ProgressBar is reset to 0 when a save starts, so that it shows this save's progress instead of the end of the previous one; on Android it now also advances during the save, because the library reports intermediate progress only for a stream that can seek.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e static button counts files only where System.IO sees them all (S-FOLDERPICKER)

"Pick Folder using static FolderPicker" counted the picked folder's files with Directory.EnumerateFiles(Folder.Path). On Android 10 and later, scoped storage keeps System.IO from listing files that other apps put in a shared folder, so the count was 0 for a DCIM folder holding 3 files (RAN on Android 36). The count is no longer shown there, and the alert says why. On Android 9 and earlier, iOS, Mac Catalyst and Windows the count is still shown, now inside a try block: a folder that the app is not allowed to list shows "Files count: not available, <reason>" instead of an exception escaping the command.

The results were long toasts, which Android 12 and later cut after two lines: "Folder picked: Name - Music, Path - /storage/emulated/0/Music, Files count …" (RAN on Android 36), and a longer path was cut even without the count. The three buttons now show the picked folder in an alert ("Folder picked", with "Name: …", "Path: …" and, for the static button, "Files count: …"), which shows the whole text on every platform. A canceled or failed pick still shows the toast "Folder is not picked, <reason>".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… anchored snackbar is visible and does not cover Back (S-SNACKBAR-MODAL)

The modal page of SnackbarPage stacked "Display Snackbar" and "Back to Snackbar MainPage" at the top of the page, while the snackbar, which is anchored to "Display Snackbar", says that it is anchored to the button on the bottom. #2456 meant to move that button to the bottom; its last commit put both buttons in a VerticalStackLayout at the top.

A snackbar anchored to a view appears above it. With the anchor at the top of the page:
- Android 36 and 28: Material placed the snackbar above the anchor, outside the page; only a strip of it (API 36) or nothing (API 28) was visible.
- iOS and Mac Catalyst: there was no room above the anchor, so AlertView placed the snackbar below it, over "Back to Snackbar MainPage" for its 5 seconds; clicks on Back did nothing until it went away.

The buttons are now at the bottom of the modal page, "Display Snackbar" above "Back to Snackbar MainPage", so the snackbar appears above its anchor and Back stays visible and usable. The snackbar's text names the button it is anchored to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Color" instead of a missing glyph, and the page says what Windows does not apply (S-SNACKBAR-WINDOWS-GLYPH)

The custom snackbar's action is the Font Awesome Microsoft logo, a private-use character drawn with ActionButtonFont. Windows shows a snackbar as an app notification, which draws its button text in the system font and does not use SnackbarOptions, so the button showed an empty box, and the red background, green text, corner radius and fonts did not appear either.

On Windows the action is now the text "Change Color" (it gives "Display Custom Snackbar" a random color); the other platforms keep the glyph. The Windows note on the page says that the SnackbarOptions (colors, fonts, corner radius) and the anchor are not applied there. Its Grid row is Auto, so the longer note can wrap; on the other platforms, where the note is hidden, the row no longer leaves an empty 20-point band.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Page hide the page that opened them (S-ALERTS-MODAL-WINDOWS-TRANSPARENT)

On Windows the implicit Page style of the Sample gives every Page a Transparent BackgroundColor, so that the Mica backdrop of the window shows through it. .NET MAUI on Windows removes the page below a modal page only when the modal page has no BackgroundColor and no Background; otherwise it keeps that page in the window. The modal pages of SnackbarPage and ToastPage are ContentPages, which the style also made Transparent, so SnackbarPage or ToastPage stayed visible under them: the modal page's buttons were drawn over the page's description text (SnackbarPage), and the buttons of the page below were still shown.

These two modal pages have no BackgroundColor on Windows, so they replace the page below and show the Mica backdrop, like the modal NavigationPage of PopupsPage (the NavigationPage style sets no BackgroundColor). The other platforms keep the style's background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stead of crashing the app (S-COLORANIM-RATE0)

The page passed every Rate that uint.TryParse accepts to BackgroundColorTo and TextColorTo, 0 included. .NET MAUI's Tweener divides by the rate on every frame of the animation, so tapping Animate with Rate 0 ended the app with a DivideByZeroException thrown on the platform's animation ticker, where no try/catch of the page can catch it (iOS, and Android when the system animations are on). A Rate of 0 now falls back to the default of 16 ms, like text that is not a number already did, so the page never passes a rate the extension methods cannot animate with (with LIB-COLORANIM-RATE0 they throw an ArgumentOutOfRangeException for it, which would also end the app from the page's async void Clicked handler).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… Android's gesture area (S-GRAVATAR-INSET)

The page removes its own top and bottom padding so that its ScrollView fills the page, and the scrolled content had top padding only, so it ended with the Cache Validity Slider. On Android 15 and later the Sample is drawn edge to edge: scrolled to the end, the Slider's bottom edge touched the top of the navigation bar, and its lower part was inside the area at the bottom of the screen where Android takes system gestures. After the soft keyboard had been used (typing into the Email entry, then closing the keyboard with Back), the ScrollView kept less room or none for the navigation bar, and the Slider could end at the bottom edge of the screen, under the gesture handle, as reported (Android 36 emulator, gesture navigation).

On Android the content now ends with 48 dp of bottom padding: the height of the three-button navigation bar, and more than the gesture area (32 dp on the Android 36 emulator), so the Slider stays clear of both even when the navigation bar inset is not applied. The other platforms keep their padding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ortedException of Mac Catalyst instead of doing nothing

KeyboardExtensions.IsSoftKeyboardShowing throws NotSupportedException on Mac Catalyst ("MacOS does not currently support opening the SoftKeyboard"), as ShowKeyboardAsync and HideKeyboardAsync do. The Show and Hide commands of the page catch it and show its message, but the IsKeyboardShowing command did not, so on Mac Catalyst the exception escaped the command and the label did not change (found in the qa-merged re-walk; the same code is on main). The command now shows the message like the other two.

RAN on Mac Catalyst (Debug, this change): Extensions > KeyboardExtensions > Check Keyboard State shows "MacOS does not currently support opening the SoftKeyboard".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue corners instead of a 20x20 speck

PopupOnDisappearingPage shows RedBlueBoxPopup without a size. Its content Grid fills the popup and holds four 10x10 blue squares at its corners, so a popup sized to its content is about 20x20: the corner squares lie on top of each other and the popup is easy to miss (seen on iOS 27 and Android 36 once the popup was displayed again by the Popup fix for a popup shown in OnDisappearing of a modal page). The popup now gets the 100x100 that the Popup Layout page gives the same popup by default, so it shows the red box with its four blue corner squares.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext is shown

After a failed load the text replaces the image (4c036f32), so the image leaves the tree, where an Element loses the BindingContext it inherits, and its ImageSource loses it too. A bound source then stopped following its view model: on GravatarImageSourcePage (GravatarImageSource Email="{Binding Email}"), clearing the Email showed the text (an empty Email does not load on Android), and typing an address afterwards never showed the photo again, while Image, Button and ImageButton showed it (Android 36).

The AvatarView now gives its image its own BindingContext (OnBindingContextChanged): a BindingContext that is set stays when the Element leaves the tree.

Unit tests: a bound image source follows its view model while the text is shown, and the image is shown once it loads; a new BindingContext reaches the image source while the text is shown (both fail without the fix). Device test: a StreamImageSource whose bound Stream changes from undecodable bytes to a red PNG after the failure shows the image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hile the handler maps the image

A StreamImageSource whose stream is already there (a completed Task, as in the device test of the previous commit) fails its load
synchronously. On Windows that happens inside BorderHandler.UpdateContent, which converts the image to its platform view and only then
sets that view as the content of the border. AvatarView replaced Content with the text from inside the failure callback
(DispatchIfRequired runs inline on the UI thread): the nested UpdateContent showed the label, and the outer one then put the image back.
The Content of the AvatarView was the Label while the platform showed the empty image (RAN on the Windows 11 VM:
ImageIsShownWhenABindingChangesTheImageSourceAfterAFailure failed in 3 of 3 runs with "The native content of the avatar is Image, expected
Label"; a diagnostic run showed SourceFailedToLoad and Content = Label 18 ms after the attach started, before the handler was done). The
text fallback of 4c036f32 had this from the start: ImageSource.FromStream opens its stream with Task.Run, so the earlier tests failed
later and passed.

AvatarView now posts both content changes to the dispatcher, so that they run after the handler's mapping, and a posted change applies
only while the image still has the source whose load reported it.

Device test: TextIsShownWhenTheImageSourceFailsAtOnce (AvatarView category).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comments that the Sample regression fixes, the round 3 StatusBar stream and the re-walk fixes brought in now say why, not what:
comments that narrate the code or restate a test's name are gone (with every "Catches:" line that only restated its test), the
XML docs of test helpers and of the private StatusBarView became short // comments where they carry a reason, and long comments
keep only their reason. Only comments changed: the comment checker reports all 20 files comment-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt change

The comment above PostContentChange said that a load of an earlier source that reports after another source was set changes
nothing. The handlers cannot tell that: they capture avatarImage.Source when the load reports, and .NET MAUI reports a failure
without its source (IImageSourcePartEvents.LoadingFailed, also for a load that another source replaced). A failure that reports
after the new source was set therefore counts for the new source. What the check does guarantee is that a change does nothing
when the image gets another source between the report and the posted change, because setting ImageSource already chose the
Content for that source. The comment now says that. Comment only: the comment checker reports the file comment-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

🟡 Changes recommended

One or more issues must be addressed before approval.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread src/CommunityToolkit.Maui/Extensions/InputViewExtensions.shared.cs
TheCodeTraveler and others added 8 commits October 16, 2026 14:49
… and soft keyboard alone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, so Android no longer hides the keyboard of the view the user moved to

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SearchBar, whose focus MAUI tracks through its query editor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dismissing behavior in a modal page or Popup also hides it while the page underneath has a focused Entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.NET MAUI reports the load of an empty image source (IsEmpty: a FileImageSource without a file, a UriImageSource without a Uri, a
StreamImageSource without a stream, a GravatarImageSource without an Email) as a failure on Android, where the AvatarView has shown its text
since 4c036f32, and as a success on iOS, Mac Catalyst and Windows, whose image source services return no image for an empty source. There the
AvatarView kept the image and showed an empty circle. Brandon decided, on the finding of the Sample re-walk, that an empty image source counts as
no image everywhere: when the load of an empty source is reported as a success, the text replaces the image.

The image stays the Content until its load is reported, also for an empty source, because only an image that was shown has a handler that loads
the source again when it stops being empty: a GravatarImageSource whose bound Email a person types still shows the photo. IImageSource.IsEmpty of
the AvatarView is also true for an empty image source, and the remarks of ImageSource say so.

Unit tests: the load of an empty File, Uri, Stream and Gravatar source shows the text; a source that gets a Uri shows the image; a loaded source
that becomes empty shows the text; IImageSource.IsEmpty is true for an empty source (all fail without the change). Device tests (AvatarView
category): empty File, Uri, Stream and Gravatar sources show the text; an empty source that a binding fills shows the image; a source that a
binding empties shows the text. RAN (Debug) on the iOS 27 simulator, on Mac Catalyst and on Windows 11 (VM, unpackaged): without the change these
6 device tests fail with "The native content of the avatar is Image, expected Label"; with it they pass there and on Android 36.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he new source

.NET MAUI reports a failed load without the source that failed (IImageSourcePartEvents.LoadingFailed), also for a load that another source
replaced while it ran and that fails later, for example because it ignores its cancellation token and the server answers with an error. It also
clears the image view then, whichever load failed. The AvatarView took such a failure for the failure of the new source and replaced the image
with the text (the open edge case of the delivery review of 23c598cc: the text replaced the newer, valid image).

The AvatarView now tells the two apart with what .NET MAUI does report: LoadingStarted resets and LoadingCompleted(true) sets whether the current
load has succeeded. A failure that follows a success, with no load started in between, can only belong to a load that was replaced. .NET MAUI has
cleared the image of the new source, so the image loads again (UpdateValue of the Source) instead of the text replacing it. A failure that is
reported before the new source loaded still shows the text, until that source loads, because it cannot be told apart from the failure of the new
source itself. Each success covers one such failure, so a load that keeps failing cannot start loads in a loop.

Unit tests: the image loads again when a replaced load fails after the new source loaded (the handler is asked to update the Source); the image
shows when the new source loads after a replaced load failed; the text shows when a load fails after an earlier one succeeded (the first fails
without the change). Device test (AvatarView category): a StreamImageSource whose load ignores its token fails after a red image replaced it, and
the red image stays. RAN (Debug): without the change it fails on the iOS 27 simulator, on Mac Catalyst and on Windows 11 ("Pixel (50, 50) did
not get the color #FF0000", the text replaced the image); with it the test passes there and on Android 36.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stead of being white on the light page

With FillOption Background a RatingView paints every shape in EmptyShapeColor: the rated items on their FillColor background, the others on the
transparent background of the RatingView. EmptyShapeColor was White on the review average, on the star of the trust review card, on the 4.9
review and on the negative review, so in the Light theme the stars of the unrated items were white on the white page (the card is white too) and
could not be seen (found in the Sample re-walk on iOS and Android; the same on main). Brandon decided to make them theme aware.

EmptyShapeColor of these four RatingViews is now an AppThemeBinding, Gray300 in Light and Gray500 in Dark, which also colors the stars on the
green and red squares. The black border of the 4.9 review follows the theme as well (white in Dark, where black disappears into the page). The
circles with a green border, the heart in the white Border and the golden stars are unchanged: they show in both themes already.

RAN on the iOS 27 simulator (Debug) in Light and in Dark: every unrated star shows on the page and on the card in both themes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re, and a blank line between the nested test classes

Review cleanup of f23e0565 and 4978c33d, comments and whitespace only (tools/comment-check: both files OK, no code token changed).
The device test explained its fixed wait twice, on failureReportTimeout and again where it waits; the comment on the constant stays.
RecordingViewHandler followed the nested AvatarViewModel without the blank line that separates the other members of the file.

Unit tests (Debug, AvatarView classes): 67 of 67 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Light theme

Gray300 on the light page is about 2.2:1, below the 3:1 contrast WCAG asks
for graphics; Gray400 reaches 3:1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

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.

🔵 Needs a closer look

Camera-switch handling can undo an explicit stop, and popup-option precedence documentation remains inaccurate.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change This label is used for PRs that include a breaking change

Projects

None yet

2 participants