Skip to content

Commit 59b239e

Browse files
RedthCopilot
andcommitted
Prove Controls remaps execute, and mark the IImageSourcePaint seam
Key presence is cheap to satisfy and proves little, so dispatch the Controls-remapped properties and the focus commands through the real composed mappers on real handlers bound to real Controls views. Each dispatch first asserts the key actually resolves, otherwise UpdateValue on an unknown key is a silent no-op and the test would pass vacuously - verified by removing TizenLabelHandler's chain and watching it fail. TizenLabelHandler is no longer excluded from the reachability theory. Core fixed it in f90ba12: FormattedText, TextType, LineBreakMode, MaxLines and TextTransform all resolve now, with no cast failures, so the exclusion note was stale. Upstream dotnet/maui#37864 is still open, so the image-background workaround stays. Mark the exact adoption point in UpdateBackground instead, with the image-first ordering the fix requires spelled out, and have the expiry test report the shipped shape of IImageSourcePaint.ImageSource so the adopter learns in one run whether the planned pattern still applies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8b0524d9-c874-4468-bff6-d21f31c772de
1 parent 4e76cad commit 59b239e

4 files changed

Lines changed: 159 additions & 9 deletions

File tree

src/Maui.Tizen.Core/Platform/Tizen/TizenPlatformExtensions.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,23 @@ public static void UpdateBackground(this TizenNativeView platformView, Paint? pa
205205
return;
206206
}
207207

208+
// ADOPTION SEAM for dotnet/maui#37864 (public read-only IImageSourcePaint).
209+
//
210+
// The image case must be matched HERE, before the solid/ToColor branches below, or an
211+
// image paint keeps flattening to a colour exactly as it does today:
212+
//
213+
// if (paint is IImageSourcePaint image)
214+
// {
215+
// platformView.UpdateBackgroundImageSourceAsync(image.ImageSource, provider)
216+
// .FireAndForgetOnUiThread();
217+
// return;
218+
// }
219+
//
220+
// UpdateBackgroundImageSourceAsync already exists and works (ViewExtensions.cs); the
221+
// only missing piece is a public way to detect that the paint is an image at all.
222+
// Deliberately NOT done by reflecting over MAUI's internal ImageSourcePaint - see
223+
// UpstreamGapExpiryTests, which fails when the contract ships so this cannot be missed.
224+
208225
if (paint is SolidPaint solid && solid.Color is Color color)
209226
{
210227
platformView.UpdateBackgroundColor(color.ToTizen());

tests/Maui.Tizen.Core.UnitTests/TizenControlHandlers.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,29 @@ public static IReadOnlySet<string> GetNeutralMapperKeys(string neutralHandlerNam
8282
return GetMapperKeys(handlerType);
8383
}
8484

85+
/// <summary>
86+
/// Resolves a command from a handler's public static <c>CommandMapper</c>.
87+
/// </summary>
88+
/// <remarks>
89+
/// <c>CommandMapper</c> exposes no key enumeration, only <c>GetCommand</c>, so a command's
90+
/// presence can only be established by resolving it.
91+
/// </remarks>
92+
public static Delegate? GetCommandMapperCommand(Type handlerType, string key)
93+
{
94+
ControlsRemap.Force();
95+
96+
var field = handlerType.GetField("CommandMapper", BindingFlags.Public | BindingFlags.Static)
97+
?? throw new InvalidOperationException($"{handlerType.Name} has no public static 'CommandMapper' field.");
98+
99+
var mapper = field.GetValue(null)
100+
?? throw new InvalidOperationException($"{handlerType.Name}.CommandMapper is null.");
101+
102+
var getCommand = mapper.GetType().GetMethod("GetCommand", [typeof(string)])
103+
?? throw new InvalidOperationException($"{handlerType.Name}.CommandMapper has no GetCommand(string).");
104+
105+
return (Delegate?)getCommand.Invoke(mapper, [key]);
106+
}
107+
85108
/// <summary>A control handler and the MAUI types it must stay in step with.</summary>
86109
/// <param name="HandlerType">The Tizen handler.</param>
87110
/// <param name="VirtualViewType">The MAUI interface it serves.</param>

tests/Maui.Tizen.Core.UnitTests/TizenHandlerMapperTests.cs

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,16 @@ public void EveryChainedMappingInvokesWithoutCastFailure(TizenControlHandlers.Co
113113
/// Asserted by observing a key that only exists once Controls has remapped, rather than
114114
/// by inspecting the chain structure - it is the reachability that matters.
115115
/// </remarks>
116-
/// <remarks>
117-
/// <c>TizenLabelHandler</c> is deliberately absent: it belongs to the core slice, still
118-
/// chains <c>TizenViewMappers.ViewMapper</c> rather than <c>LabelHandler.Mapper</c>, and
119-
/// therefore does not yet receive <c>FormattedText</c>, <c>TextType</c>,
120-
/// <c>LineBreakMode</c>, <c>MaxLines</c> or <c>TextTransform</c>. Asserting it here would
121-
/// fail on someone else's in-flight file; it is reported to the core slice instead.
122-
/// </remarks>
123116
[Theory]
124117
[InlineData(typeof(TizenCheckBoxHandler), "Color")]
125118
[InlineData(typeof(TizenButtonHandler), "IsInAccessibleTree")]
126119
[InlineData(typeof(TizenEntryHandler), "Description")]
127120
[InlineData(typeof(TizenPickerHandler), "Hint")]
121+
[InlineData(typeof(TizenPickerHandler), "ItemsSource")]
122+
[InlineData(typeof(TizenStepperHandler), "Increment")]
123+
[InlineData(typeof(TizenLabelHandler), "FormattedText")]
124+
[InlineData(typeof(TizenLabelHandler), "TextType")]
125+
[InlineData(typeof(TizenLabelHandler), "MaxLines")]
128126
public void ControlsRemappedKeysReachTheBackend(Type handlerType, string key)
129127
{
130128
ControlsRemap.Force();
@@ -219,6 +217,105 @@ public void ViewCommandKeysMatchTheTizenBaseMapper()
219217
}
220218
}
221219

220+
/// <summary>
221+
/// The Controls-remapped keys and the focus commands actually <em>execute</em> against a
222+
/// Tizen handler, not merely resolve.
223+
/// </summary>
224+
/// <remarks>
225+
/// <para>
226+
/// Key presence and command presence are cheap to satisfy and prove little - a chained
227+
/// mapper makes every key resolve. This dispatches through the real composed mappers on a
228+
/// real handler bound to a real Controls virtual view, so a mapping that resolves to
229+
/// something uncallable fails here.
230+
/// </para>
231+
/// <para>
232+
/// <see cref="InvalidCastException"/> is the failure that matters: MAUI's static mappers are
233+
/// closed over its concrete handler type, so a key this backend has not overridden throws
234+
/// rather than no-ops. Off-platform no-op side effects are ignored, since the host lane has
235+
/// no NUI.
236+
/// </para>
237+
/// </remarks>
238+
[Fact]
239+
public void ControlsRemappedPropertiesAndFocusCommandsExecute()
240+
{
241+
ControlsRemap.Force();
242+
243+
var label = new Controls.Label();
244+
var labelHandler = new TizenLabelHandler();
245+
labelHandler.SetVirtualView(label);
246+
247+
// FormattedText is contributed exclusively by Label.RemapForControls.
248+
DispatchProperty(labelHandler, label, "FormattedText");
249+
DispatchProperty(labelHandler, label, "TextType");
250+
251+
var entry = new Controls.Entry();
252+
var entryHandler = new TizenEntryHandler();
253+
entryHandler.SetVirtualView(entry);
254+
255+
foreach (var command in new[] { nameof(IView.Focus), nameof(IView.Unfocus) })
256+
DispatchCommand(entryHandler, entry, command, new FocusRequest());
257+
258+
// The two keys that were genuinely throwing before they were given Tizen bodies.
259+
var picker = new Controls.Picker();
260+
var pickerHandler = new TizenPickerHandler();
261+
pickerHandler.SetVirtualView(picker);
262+
DispatchProperty(pickerHandler, picker, "ItemsSource");
263+
264+
var stepper = new Controls.Stepper();
265+
var stepperHandler = new TizenStepperHandler();
266+
stepperHandler.SetVirtualView(stepper);
267+
DispatchProperty(stepperHandler, stepper, "Increment");
268+
}
269+
270+
static void DispatchProperty(IElementHandler handler, IElement view, string key)
271+
{
272+
// Without this the test would be vacuous: UpdateValue on a key no mapper defines does
273+
// nothing at all and passes silently.
274+
Assert.True(
275+
TizenControlHandlers.GetMapperKeys(handler.GetType()).Contains(key),
276+
$"{handler.GetType().Name}.Mapper does not define '{key}', so dispatching it is a " +
277+
"no-op and this test would prove nothing.");
278+
279+
try
280+
{
281+
handler.UpdateValue(key);
282+
}
283+
catch (InvalidCastException ex)
284+
{
285+
Assert.Fail(
286+
$"Dispatching '{key}' to {handler.GetType().Name} threw InvalidCastException: " +
287+
$"{ex.Message}. The key resolves through MAUI's chained mapper, which is closed " +
288+
"over MAUI's concrete handler, so this backend must override it.");
289+
}
290+
catch (Exception)
291+
{
292+
// An off-platform no-op stand-in has no NUI to touch; only a cast failure means the
293+
// composition itself is wrong.
294+
}
295+
}
296+
297+
static void DispatchCommand(IElementHandler handler, IElement view, string key, object? args)
298+
{
299+
Assert.True(
300+
TizenControlHandlers.GetCommandMapperCommand(handler.GetType(), key) is not null,
301+
$"{handler.GetType().Name}.CommandMapper does not resolve '{key}', so invoking it " +
302+
"is a no-op and this test would prove nothing.");
303+
304+
try
305+
{
306+
handler.Invoke(key, args);
307+
}
308+
catch (InvalidCastException ex)
309+
{
310+
Assert.Fail(
311+
$"Invoking command '{key}' on {handler.GetType().Name} threw " +
312+
$"InvalidCastException: {ex.Message}.");
313+
}
314+
catch (Exception)
315+
{
316+
}
317+
}
318+
222319
/// <summary>
223320
/// A mapping referenced by a mapper must not be declared inside <c>#if TIZEN</c>.
224321
/// </summary>

tests/Maui.Tizen.Core.UnitTests/UpstreamGapExpiryTests.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,26 @@ public void ImageSourcePaintIsStillInternal()
6060
assembly.GetType("Microsoft.Maui.IImageSourcePaint")
6161
?? assembly.GetType("Microsoft.Maui.Graphics.IImageSourcePaint");
6262

63+
// Reported in the failure message so the adopter learns in one run whether the shipped
64+
// shape still matches the planned pattern match, rather than discovering it mid-edit.
65+
var imageSourceProperty = publicContract?.GetProperty("ImageSource");
66+
var shape = publicContract is null
67+
? string.Empty
68+
: imageSourceProperty is null
69+
? "\n\nNOTE: the shipped interface has NO 'ImageSource' property, so the planned " +
70+
"adoption below does not apply as written - re-read the final upstream shape first."
71+
: $"\n\nShipped shape: {imageSourceProperty.PropertyType.Name} ImageSource " +
72+
$"{{ {(imageSourceProperty.CanRead ? "get; " : "")}{(imageSourceProperty.CanWrite ? "set; " : "")}}} " +
73+
"- matches the planned consumption-only adoption.";
74+
6375
Assert.True(
6476
publicContract is null,
6577
$"""
6678
MAUI now exposes '{publicContract?.FullName}'. This is the upstream fix from
6779
dotnet/maui#37864 landing, and the image-background workaround should now be removed.
6880
69-
Adopt it by pattern matching image-first in the background mapping:
81+
Adopt it at the ADOPTION SEAM comment in TizenPlatformExtensions.UpdateBackground, by
82+
pattern matching image-first:
7083
7184
if (paint is IImageSourcePaint imagePaint)
7285
// route imagePaint.ImageSource through UpdateBackgroundImageSourceAsync
@@ -75,7 +88,7 @@ MAUI now exposes '{publicContract?.FullName}'. This is the upstream fix from
7588
Match the image case BEFORE the solid/ToColor fallback, or an image paint keeps
7689
flattening to a colour exactly as it does today. Use the interface directly - no
7790
reflection and no internal types. Then delete this test and update the
78-
"MAUI extensibility blockers" section of docs/wave-a-handlers.md.
91+
"MAUI extensibility blockers" section of docs/wave-a-handlers.md.{shape}
7992
""");
8093

8194
// The gap is only real while the concrete type is genuinely inaccessible. If this

0 commit comments

Comments
 (0)