From 321a654a45dad6df128294f569eb5e57f70cdb05 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 26 Jun 2026 00:59:30 -0700 Subject: [PATCH 1/5] perf(reconciler): skip per-cell automation-name round-trip on unchanged caption (P3) UpdateDefaultAutomationName ran a UIA GetName read + SetName write on every changed cell that goes through Update, even when the caption was unchanged - where the resulting Name write is a value no-op (or hits the author-override guard the GetName already protects). Add a caption-only fast-path that returns before touching the DP when the new caption is empty/whitespace or equals the old caption, and factor the remaining decision into a pure, DP-free ResolveDefaultAutomationNameUpdate helper so the caption/override policy is unit-testable headlessly. On a changed caption the original GetName + author-override + SetName path runs unchanged, so a genuine caption change still flows to UIA and author-set names are never clobbered. Scope is Reconciler.cs only (file-disjoint from the P1 PR #692). New headless tests pin the decision, including a teeth case: reverting the unchanged-caption skip makes the empty-live-Name assertion return the caption instead of null and fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Reactor/Core/Reconciler.cs | 29 +++++- .../ReconcilerAutomationNameTests.cs | 98 +++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 tests/Reactor.Tests/ReconcilerAutomationNameTests.cs diff --git a/src/Reactor/Core/Reconciler.cs b/src/Reactor/Core/Reconciler.cs index fc2983b18..75c448acb 100644 --- a/src/Reactor/Core/Reconciler.cs +++ b/src/Reactor/Core/Reconciler.cs @@ -2683,14 +2683,37 @@ public static void ApplyDefaultAutomationName(FrameworkElement fe, string? capti public static void UpdateDefaultAutomationName(FrameworkElement fe, string? oldCaption, string? newCaption) { if (fe is null) return; + // P3 fast-path — skip the UIA GetName read + SetName write whenever the + // outcome is independent of the live Name: an empty/whitespace new caption + // (nothing to write), or an unchanged caption (the Name already reflects it + // from mount or a prior update, or the author overrode it — either way + // nothing to do). These two arms mirror the caption-only arms of + // ResolveDefaultAutomationNameUpdate, so the live path and the pure + // decision helper stay in lockstep. On a changed caption we fall through to + // the original GetName + author-override + SetName behavior, unchanged. if (string.IsNullOrWhiteSpace(newCaption)) return; + if (string.Equals(oldCaption, newCaption, StringComparison.Ordinal)) return; var current = Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(fe); + var resolved = ResolveDefaultAutomationNameUpdate(current, oldCaption, newCaption); + if (resolved is not null) + Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(fe, resolved); + } + + // Pure decision for UpdateDefaultAutomationName — no DP/UIA interop, so the + // caption/override policy is unit-testable headlessly (Reactor.Tests). Returns + // the Name to write, or null to leave the live automation Name untouched. Kept + // in sync with the UpdateDefaultAutomationName fast-path above: the whitespace- + // new-caption and unchanged-caption arms here are the same skips the live path + // takes before it ever reads the current Name. + internal static string? ResolveDefaultAutomationNameUpdate(string? current, string? oldCaption, string? newCaption) + { + if (string.IsNullOrWhiteSpace(newCaption)) return null; + if (string.Equals(oldCaption, newCaption, StringComparison.Ordinal)) return null; bool authorOverride = !string.IsNullOrEmpty(current) && (oldCaption is null || !string.Equals(current, oldCaption, StringComparison.Ordinal)); - if (authorOverride) return; - var trimmed = newCaption.Length > 100 ? newCaption.Substring(0, 100) : newCaption; - Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(fe, trimmed); + if (authorOverride) return null; + return newCaption.Length > 100 ? newCaption.Substring(0, 100) : newCaption; } internal static string? ExtractElementCaption(Element? element) => element switch diff --git a/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs b/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs new file mode 100644 index 000000000..22821fb91 --- /dev/null +++ b/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs @@ -0,0 +1,98 @@ +using System; +using Microsoft.UI.Reactor.Core; +using Xunit; + +namespace Microsoft.UI.Reactor.Tests; + +/// +/// Pins the decision policy of via its +/// pure, DP-free helper (P3 — trim +/// the per-cell automation round-trip). +/// +/// The optimization: when the caption is unchanged (or empty/whitespace) the live method skips +/// the UIA GetName read + SetName write entirely. These tests assert that policy +/// AND that the two correctness-critical guarantees survive: an author-set automation Name is +/// never clobbered, and a genuine caption change still flows through to the Name. +/// +public class ReconcilerAutomationNameTests +{ + // ──────────────────────────────────────────────────────────────── + // The P3 skip (teeth: revert the unchanged-caption fast-path → these flip) + // ──────────────────────────────────────────────────────────────── + + [Fact] + public void Unchanged_Caption_Returns_Null_When_Name_Already_Matches() + { + // Name already reflects the caption → nothing to write. + Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "X", oldCaption: "X", newCaption: "X")); + } + + [Fact] + public void Unchanged_Caption_Returns_Null_Even_When_Live_Name_Is_Empty() + { + // TEETH for P3: with the `oldCaption == newCaption` fast-path the unchanged caption is a + // skip regardless of the live Name. Remove that line and the helper falls through to the + // author-override logic: current "" is not an override, so it returns "X" (a write) and + // this assertion fails — i.e. reverting the optimization breaks this test. + Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "", oldCaption: "X", newCaption: "X")); + } + + [Theory] + [InlineData("X", null)] + [InlineData("X", "")] + [InlineData("X", " ")] + public void Empty_Or_Whitespace_New_Caption_Returns_Null(string? current, string? newCaption) + { + // No caption to project onto the Name → never touch it (matches the original guard). + Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current, oldCaption: "anything", newCaption)); + } + + // ──────────────────────────────────────────────────────────────── + // Author-override preservation (MED-risk invariant — must not regress) + // ──────────────────────────────────────────────────────────────── + + [Fact] + public void Author_Override_Survives_Caption_Change() + { + // The live Name ("custom") differs from the previous caption ("A") → the author set it. + // A caption change A→B must NOT clobber the author's value. + Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "custom", oldCaption: "A", newCaption: "B")); + } + + [Fact] + public void Author_Override_Survives_When_Old_Caption_Unknown() + { + // oldCaption null but a non-empty live Name is present → treat as author-owned, leave it. + Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "custom", oldCaption: null, newCaption: "B")); + } + + // ──────────────────────────────────────────────────────────────── + // The default still follows a genuine caption change + // ──────────────────────────────────────────────────────────────── + + [Fact] + public void Default_Follows_Caption_Change() + { + // Live Name equals the previous caption ("A") → our default owns it → update to "B". + Assert.Equal("B", Reconciler.ResolveDefaultAutomationNameUpdate(current: "A", oldCaption: "A", newCaption: "B")); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void FirstTime_Set_When_Live_Name_Empty_And_Caption_Changed(string? current) + { + // No author Name yet + a real (changed) caption → set it. + Assert.Equal("B", Reconciler.ResolveDefaultAutomationNameUpdate(current, oldCaption: "A", newCaption: "B")); + } + + [Fact] + public void Long_Changed_Caption_Is_Trimmed_To_100_Chars() + { + var longCaption = new string('a', 250); + var resolved = Reconciler.ResolveDefaultAutomationNameUpdate(current: "old", oldCaption: "old", newCaption: longCaption); + Assert.NotNull(resolved); + Assert.Equal(100, resolved!.Length); + Assert.Equal(new string('a', 100), resolved); + } +} From 6416cb455b821cc53685134900e4ce403d967cc0 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 26 Jun 2026 01:19:27 -0700 Subject: [PATCH 2/5] fix(reconciler): preserve restore-default in P3 via idempotent-write guard Addresses Copilot review on #695. The first cut skipped UpdateDefaultAutomationName whenever oldCaption == newCaption. That is unsafe: ApplyModifiers runs BEFORE this (Update.cs:188-200) and can clear AutomationProperties.Name when an explicit .AutomationName() override is removed - even though the caption is unchanged. The blanket skip then left UIA Name empty instead of restoring the caption-derived default (which main does). Replace the unchanged-caption skip with an idempotent-write guard: keep main's GetName + author-override logic verbatim, compute the trimmed target, and skip only the SetName when the live Name already equals it (a value no-op). This still WRITES when the default must be (re)applied - including the cleared-Name-unchanged-caption restore case - so behavior is identical to main minus the redundant same-value write. Tests rewritten: the teeth now pin the idempotent guard (revert it -> the skip test sees a redundant 'X' write and fails) AND the restore-default case (a blanket unchanged-caption skip -> the restore test returns null and fails). Added an author-override-survives-unchanged-caption case. Full Reactor.Tests 9714 passed / 0 failed / 64 skipped; core-lib Release AOT 0W/0E. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Reactor/Core/Reconciler.cs | 33 ++++++++------- .../ReconcilerAutomationNameTests.cs | 42 ++++++++++++------- 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/Reactor/Core/Reconciler.cs b/src/Reactor/Core/Reconciler.cs index 75c448acb..2470a9a38 100644 --- a/src/Reactor/Core/Reconciler.cs +++ b/src/Reactor/Core/Reconciler.cs @@ -2683,16 +2683,11 @@ public static void ApplyDefaultAutomationName(FrameworkElement fe, string? capti public static void UpdateDefaultAutomationName(FrameworkElement fe, string? oldCaption, string? newCaption) { if (fe is null) return; - // P3 fast-path — skip the UIA GetName read + SetName write whenever the - // outcome is independent of the live Name: an empty/whitespace new caption - // (nothing to write), or an unchanged caption (the Name already reflects it - // from mount or a prior update, or the author overrode it — either way - // nothing to do). These two arms mirror the caption-only arms of - // ResolveDefaultAutomationNameUpdate, so the live path and the pure - // decision helper stay in lockstep. On a changed caption we fall through to - // the original GetName + author-override + SetName behavior, unchanged. + // A whitespace new caption has nothing to write — skip the GetName read too + // (main's first line returns on whitespace newCaption, so this is behavior- + // identical, it just avoids the interop read). Otherwise read the live Name + // and let the pure helper decide; we only touch the DP when it returns a value. if (string.IsNullOrWhiteSpace(newCaption)) return; - if (string.Equals(oldCaption, newCaption, StringComparison.Ordinal)) return; var current = Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(fe); var resolved = ResolveDefaultAutomationNameUpdate(current, oldCaption, newCaption); if (resolved is not null) @@ -2701,19 +2696,27 @@ public static void UpdateDefaultAutomationName(FrameworkElement fe, string? oldC // Pure decision for UpdateDefaultAutomationName — no DP/UIA interop, so the // caption/override policy is unit-testable headlessly (Reactor.Tests). Returns - // the Name to write, or null to leave the live automation Name untouched. Kept - // in sync with the UpdateDefaultAutomationName fast-path above: the whitespace- - // new-caption and unchanged-caption arms here are the same skips the live path - // takes before it ever reads the current Name. + // the Name to write, or null to leave the live automation Name untouched. Models + // main's GetName + author-override + SetName logic exactly, plus one safe saving: + // the idempotent-write guard below. internal static string? ResolveDefaultAutomationNameUpdate(string? current, string? oldCaption, string? newCaption) { if (string.IsNullOrWhiteSpace(newCaption)) return null; - if (string.Equals(oldCaption, newCaption, StringComparison.Ordinal)) return null; bool authorOverride = !string.IsNullOrEmpty(current) && (oldCaption is null || !string.Equals(current, oldCaption, StringComparison.Ordinal)); if (authorOverride) return null; - return newCaption.Length > 100 ? newCaption.Substring(0, 100) : newCaption; + var trimmed = newCaption.Length > 100 ? newCaption.Substring(0, 100) : newCaption; + // Idempotent-write guard — the P3 saving. When the live Name already equals the + // caption-derived default, the SetName main would issue is a value no-op, so skip + // it (this is the steady-state hot path: an unchanged caption means current == + // trimmed). Crucially we still WRITE when the default must be (re)applied — e.g. + // a modifier removal this render cleared the Name (current empty) even though the + // caption itself is unchanged — so a removed .AutomationName() override correctly + // falls back to the caption default, matching main. Behaviorally identical to main + // minus the redundant same-value write. + if (string.Equals(current, trimmed, StringComparison.Ordinal)) return null; + return trimmed; } internal static string? ExtractElementCaption(Element? element) => element switch diff --git a/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs b/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs index 22821fb91..cbd72c3df 100644 --- a/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs +++ b/tests/Reactor.Tests/ReconcilerAutomationNameTests.cs @@ -7,34 +7,40 @@ namespace Microsoft.UI.Reactor.Tests; /// /// Pins the decision policy of via its /// pure, DP-free helper (P3 — trim -/// the per-cell automation round-trip). +/// the redundant per-cell automation write). /// -/// The optimization: when the caption is unchanged (or empty/whitespace) the live method skips -/// the UIA GetName read + SetName write entirely. These tests assert that policy -/// AND that the two correctness-critical guarantees survive: an author-set automation Name is -/// never clobbered, and a genuine caption change still flows through to the Name. +/// The optimization is an idempotent-write guard: the live method still reads the UIA Name, but +/// skips the SetName write when the Name already equals the caption-derived default (the +/// steady-state hot path). These tests pin that saving AND the three correctness guarantees that +/// must survive it: an author-set Name is never clobbered, a genuine caption change still flows +/// through, and a Name cleared this render (e.g. a removed .AutomationName() override) is +/// restored to the caption default even when the caption itself is unchanged. /// public class ReconcilerAutomationNameTests { // ──────────────────────────────────────────────────────────────── - // The P3 skip (teeth: revert the unchanged-caption fast-path → these flip) + // The P3 idempotent-write guard (teeth: revert it → these flip) // ──────────────────────────────────────────────────────────────── [Fact] - public void Unchanged_Caption_Returns_Null_When_Name_Already_Matches() + public void Unchanged_Caption_Skips_Write_When_Name_Already_Matches() { - // Name already reflects the caption → nothing to write. + // Steady-state hot path: the live Name already equals the caption-derived default, so the + // SetName main would issue is a value no-op → return null (skip). + // TEETH: remove the `current == trimmed` guard and the helper falls through to + // `return trimmed` ("X", a redundant write) → this assertion fails. Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "X", oldCaption: "X", newCaption: "X")); } [Fact] - public void Unchanged_Caption_Returns_Null_Even_When_Live_Name_Is_Empty() + public void Cleared_Name_Restores_Caption_Default_When_Caption_Unchanged() { - // TEETH for P3: with the `oldCaption == newCaption` fast-path the unchanged caption is a - // skip regardless of the live Name. Remove that line and the helper falls through to the - // author-override logic: current "" is not an override, so it returns "X" (a write) and - // this assertion fails — i.e. reverting the optimization breaks this test. - Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "", oldCaption: "X", newCaption: "X")); + // Regression guard (the bug a blanket unchanged-caption skip would introduce): a removed + // `.AutomationName()` override makes ApplyModifiers clear the live Name to empty *before* + // this runs, even though the caption is unchanged. The default must be re-applied — so an + // empty current with an unchanged caption "X" resolves to a WRITE of "X", matching main. + // TEETH the other way: re-add `if (oldCaption == newCaption) return null;` → this fails. + Assert.Equal("X", Reconciler.ResolveDefaultAutomationNameUpdate(current: "", oldCaption: "X", newCaption: "X")); } [Theory] @@ -66,6 +72,14 @@ public void Author_Override_Survives_When_Old_Caption_Unknown() Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "custom", oldCaption: null, newCaption: "B")); } + [Fact] + public void Author_Override_Survives_Unchanged_Caption() + { + // Unchanged caption "X" but the live Name is an author override ("custom" ≠ oldCaption) → + // the idempotent guard must NOT fire (custom ≠ trimmed "X"); author-override wins → null. + Assert.Null(Reconciler.ResolveDefaultAutomationNameUpdate(current: "custom", oldCaption: "X", newCaption: "X")); + } + // ──────────────────────────────────────────────────────────────── // The default still follows a genuine caption change // ──────────────────────────────────────────────────────────────── From 31302db03e018ce6c9559a2e8e705223237bc449 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 26 Jun 2026 01:28:14 -0700 Subject: [PATCH 3/5] docs(reconciler): clarify P3 method comment per review GetName still runs for non-whitespace captions; the P3 saving is the skipped redundant SetName inside the helper. Reword the comment so it no longer implies the GetName read is removed for unchanged captions (Copilot review on #695). Comment-only; no behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Reactor/Core/Reconciler.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Reactor/Core/Reconciler.cs b/src/Reactor/Core/Reconciler.cs index 2470a9a38..69e59b1a2 100644 --- a/src/Reactor/Core/Reconciler.cs +++ b/src/Reactor/Core/Reconciler.cs @@ -2683,10 +2683,14 @@ public static void ApplyDefaultAutomationName(FrameworkElement fe, string? capti public static void UpdateDefaultAutomationName(FrameworkElement fe, string? oldCaption, string? newCaption) { if (fe is null) return; - // A whitespace new caption has nothing to write — skip the GetName read too - // (main's first line returns on whitespace newCaption, so this is behavior- - // identical, it just avoids the interop read). Otherwise read the live Name - // and let the pure helper decide; we only touch the DP when it returns a value. + // Two arms: a whitespace new caption has nothing to write, so we return before + // even reading the Name (main also returns first on whitespace newCaption, so + // this is behavior-identical — it just avoids that one interop read). For any + // NON-whitespace caption we still read the live Name and let the pure helper + // decide. The actual P3 saving lives inside that helper: it skips the SetName + // write when the live Name already equals the caption-derived default (the + // redundant same-value write). GetName is NOT skipped for unchanged captions — + // only the redundant SetName is. We touch the DP only when the helper returns a value. if (string.IsNullOrWhiteSpace(newCaption)) return; var current = Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(fe); var resolved = ResolveDefaultAutomationNameUpdate(current, oldCaption, newCaption); From c608aaf77270627b211a1a48e6acfbc08a3913c4 Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 26 Jun 2026 02:31:35 -0700 Subject: [PATCH 4/5] test(selftest): pin live restore-default automation-name seam (P3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pr-review (test-coverage L1) flagged that the P3 idempotent-write guard's live UIA seam was only pinned headlessly via the pure helper ResolveDefaultAutomationNameUpdate. The subtle restore-default branch — a removed .AutomationName() override clears the live Name (ApplyModifiers ClearValue) and the guard must re-apply the caption default even though the caption is unchanged — was not proven end-to-end through a real control. Extend the CoreCov_AccessibilityModifiers selftest with a third phase that drops the .AutomationName() override (caption unchanged) and asserts AutomationProperties.GetName restores the caption-derived default, plus assertions that the author override wins at mount and a changed override still flows through. Exercises UpdateDefaultAutomationName via a live WinUI control. Teeth: a blanket unchanged-caption skip (or dropping the live SetName) leaves the Name cleared at phase 2 -> A11y_Name_RestoredToCaptionDefault flips. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SelfTest/Fixtures/CoreCoverageFixtures.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/Reactor.AppTests.Host/SelfTest/Fixtures/CoreCoverageFixtures.cs b/tests/Reactor.AppTests.Host/SelfTest/Fixtures/CoreCoverageFixtures.cs index 4d8295768..f0a203fbb 100644 --- a/tests/Reactor.AppTests.Host/SelfTest/Fixtures/CoreCoverageFixtures.cs +++ b/tests/Reactor.AppTests.Host/SelfTest/Fixtures/CoreCoverageFixtures.cs @@ -751,7 +751,7 @@ public override async Task RunAsync() .Landmark(Microsoft.UI.Xaml.Automation.Peers.AutomationLandmarkType.Main) ); } - else + else if (phase == 1) { return VStack( Button("UpdateA11y", () => set(2)), @@ -768,6 +768,19 @@ public override async Task RunAsync() .TabNavigation(Microsoft.UI.Xaml.Input.KeyboardNavigationMode.Cycle) ); } + else + { + // phase 2: the .AutomationName() override is removed while the caption + // ("Accessible") is unchanged. ApplyModifiers clears the live UIA Name; + // the P3 idempotent-write guard must still restore the caption-derived + // default (current "" != caption => a real write) rather than leave the + // Name empty. Exercises the live seam's restore-default branch end-to-end. + return VStack( + Button("UpdateA11y", () => set(3)), + TextBlock("Accessible") + .HelpText("Updated help text") + ); + } }); await Harness.Render(); @@ -776,6 +789,9 @@ public override async Task RunAsync() H.Check("A11y_Mounted", tb is not null); H.Check("A11y_HelpText", Microsoft.UI.Xaml.Automation.AutomationProperties.GetHelpText(tb!) == "This is help text"); + // The author-set .AutomationName() override wins over the caption default at mount. + H.Check("A11y_Name_Override", + Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(tb!) == "test-text"); // Update accessibility modifiers H.ClickButton("UpdateA11y"); @@ -786,6 +802,19 @@ public override async Task RunAsync() H.Check("A11y_LiveSetting", Microsoft.UI.Xaml.Automation.AutomationProperties.GetLiveSetting(tb!) == Microsoft.UI.Xaml.Automation.Peers.AutomationLiveSetting.Polite); + // A changed override still flows through (the author name updates, not the caption). + H.Check("A11y_Name_OverrideUpdated", + Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(tb!) == "test-text-updated"); + + // Remove the .AutomationName() override with the caption unchanged. ApplyModifiers + // clears the Name; the P3 guard (live seam) must restore the caption-derived default + // "Accessible" rather than leave it empty. TEETH: a blanket unchanged-caption skip + // (or dropping the live SetName) leaves the Name cleared here and this flips. + H.ClickButton("UpdateA11y"); + await Harness.Render(); + tb = H.FindText("Accessible"); + H.Check("A11y_Name_RestoredToCaptionDefault", + Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(tb!) == "Accessible"); } } From 3ef87ebd2588b4f7720c306d060decae3929dd3c Mon Sep 17 00:00:00 2001 From: Alexandre Zollinger Chohfi Date: Fri, 26 Jun 2026 06:27:30 -0700 Subject: [PATCH 5/5] docs(reconciler): scope P3 comment to the SetName write (review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reword the UpdateDefaultAutomationName comment so it attributes the conditional DP access to the SetName *write*, not a vague "touch the DP" — the GetName read always runs for a non-whitespace caption; only the SetName write is skipped when the helper returns null. Comment-only; no behavior change. Addresses the Copilot review thread at Reconciler.cs:2693. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Reactor/Core/Reconciler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Reactor/Core/Reconciler.cs b/src/Reactor/Core/Reconciler.cs index 69e59b1a2..61268073b 100644 --- a/src/Reactor/Core/Reconciler.cs +++ b/src/Reactor/Core/Reconciler.cs @@ -2690,7 +2690,8 @@ public static void UpdateDefaultAutomationName(FrameworkElement fe, string? oldC // decide. The actual P3 saving lives inside that helper: it skips the SetName // write when the live Name already equals the caption-derived default (the // redundant same-value write). GetName is NOT skipped for unchanged captions — - // only the redundant SetName is. We touch the DP only when the helper returns a value. + // only the redundant SetName is. We perform the SetName write only when the helper + // returns a value (the GetName read above always runs for a non-whitespace caption). if (string.IsNullOrWhiteSpace(newCaption)) return; var current = Microsoft.UI.Xaml.Automation.AutomationProperties.GetName(fe); var resolved = ResolveDefaultAutomationNameUpdate(current, oldCaption, newCaption);