Skip to content

Instantly share code, notes, and snippets.

@StephaneDelcroix
Created March 6, 2026 08:10
Show Gist options
  • Select an option

  • Save StephaneDelcroix/81c5e83f47ecdb3d67ce359a15547255 to your computer and use it in GitHub Desktop.

Select an option

Save StephaneDelcroix/81c5e83f47ecdb3d67ce359a15547255 to your computer and use it in GitHub Desktop.
XAML Diffing Engine — Test Catalog for XamlNodeDiff.ComputeDiff()

XAML Diffing Engine — Test Catalog

Tests for XamlNodeDiff.ComputeDiff(), the build-time diff engine that compares two XAML node trees and produces a change set of property changes (set/clear) and child list mutations (add/remove/reorder).

All XAML snippets are wrapped in <ContentPage xmlns="..." x:Class="Test.MyPage">…</ContentPage>. The Diff block shows the exact ToDebugString() output where available.


1 · Identical trees

1.1 Same instance → empty

<Label Text="Hello" TextColor="Blue" />

→ same

Diff: empty


1.2 Two parses of identical XAML → empty

<Label Text="Hello" TextColor="Blue" />

→ same

Diff: empty


1.3 Empty page → empty

<!-- no content -->

→ same

Diff: empty


2 · Property changes

2.1 Root property changed

Old: <ContentPage Title="Hello">… New: <ContentPage Title="World">…

Diff: 1 node(s) with property changes
  [root] Title = "World"
NodeChanges "" (root): Title = "World" (Set)
ChildListChanges 0

2.2 Child element property changed

Old

<Label Text="Hello" />

New

<Label Text="World" />
Diff: 1 node(s) with property changes
  [Label_0] Text = "World"

2.3 Property removed → Clear

Old: <ContentPage Title="Hello"> New: <ContentPage>

NodeChanges "" (root): Title cleared
ChildListChanges 0

2.4 Property added

Old: <ContentPage> New: <ContentPage Title="New">

NodeChanges "" (root): Title = "New" (Set)
ChildListChanges 0

2.5 Multiple properties changed on same node

Old

<Label Text="Hello" FontSize="14" />

New

<Label Text="World" FontSize="18" />
Diff: 1 node(s) with property changes
  [Label_0] Text = "World", FontSize = "18"

2.6 Multiple nodes changed

Old

<VerticalStackLayout>
    <Label Text="A" />
    <Label Text="B" />
</VerticalStackLayout>

New

<VerticalStackLayout>
    <Label Text="X" />
    <Label Text="Y" />
</VerticalStackLayout>
Diff: 2 node(s) with property changes
  [VerticalStackLayout_0/Label_0] Text = "X"
  [VerticalStackLayout_0/Label_1] Text = "Y"

2.7 Deeply nested property change

Old

<VerticalStackLayout>
    <Grid>
        <Label Text="Deep" />
    </Grid>
</VerticalStackLayout>

New: Label Text → "Changed"

Diff: 1 node(s) with property changes
  [VerticalStackLayout_0/Grid_0/Label_0] Text = "Changed"

2.8 Property added (child, debug string)

Old

<Label Text="Hello" />

New

<Label Text="Hello" FontSize="20" />
Diff: 1 node(s) with property changes
  [Label_0] FontSize = "20"

2.9 Property cleared (child, debug string)

Old

<Label Text="Hello" FontSize="14" />

New

<Label Text="Hello" />
Diff: 1 node(s) with property changes
  [Label_0] FontSize cleared

3 · Structural fallbacks (→ null)

3.1 Root element type changed

Old: <ContentPage …> New: <ContentView …>

Diff: null — full reload required


3.2 x:Name changed

Old

<Label x:Name="oldLabel" Text="Hello" />

New

<Label x:Name="newLabel" Text="Hello" />

Diff: null — x:Name generates a field in code-behind


3.3 x:DataType changed

Old: <ContentPage x:DataType="MyViewModel"> New: <ContentPage x:DataType="OtherViewModel">

Diff: null — x:DataType drives compiled bindings


3.4 x:Name identical → NOT structural

<Label x:Name="myLabel" Text="Hello" />

→ same

Diff: empty (unchanged x:Name is fine)


4 · Child reordering

4.1 Two unique types swapped

Old

<VerticalStackLayout>
    <Label Text="A" />
    <Button Text="B" />
</VerticalStackLayout>

New

<VerticalStackLayout>
    <Button Text="B" />
    <Label Text="A" />
</VerticalStackLayout>
Diff: 0 node(s) with property changes, 1 child list change(s)
  children [VerticalStackLayout_0] VerticalStackLayout_0/Button_1 → VerticalStackLayout_0/Button_0, VerticalStackLayout_0/Label_0 → VerticalStackLayout_0/Label_1

4.2 Duplicate types → positional property change

Old

<VerticalStackLayout>
    <Label Text="A" />
    <Label Text="B" />
</VerticalStackLayout>

New: Labels swapped (Text="B", Text="A")

Two Labels can't be distinguished by type — treated as positional property changes:

NodeChanges 2 — Label_0: Text="B", Label_1: Text="A"
ChildListChanges 0

4.3 Reorder + property change

Old: Label(A), Button(B)New: Button(B2), Label(A)

NodeChanges 1 — Button: Text="B2"
ChildListChanges 1 — reorder

4.4 Same order, properties change → no child list change

Old: Label(A), Button(B)New: Label(A2), Button(B2)

NodeChanges 2 — both Text changed
ChildListChanges 0

4.5 Three-element rotation

Old: Label, Button, EntryNew: Entry, Label, Button

ChildListChanges 1 — 3 entries: Entry(2→0), Label(0→1), Button(1→2)

5 · Child add / remove

5.1 Child added (simple)

Old

<VerticalStackLayout>
    <Label Text="A" />
</VerticalStackLayout>

New

<VerticalStackLayout>
    <Label Text="A" />
    <Button Text="B" />
</VerticalStackLayout>
Diff: 0 node(s) with property changes, 1 child list change(s)
  children [VerticalStackLayout_0] VerticalStackLayout_0/Label_0 (unchanged), +VerticalStackLayout_0/Button_1

5.2 Child removed

Old: Label(A), Button(B)New: Label(A)

Diff: 0 node(s) with property changes, 1 child list change(s)
  children [VerticalStackLayout_0] VerticalStackLayout_0/Label_0 (unchanged); removed: -VerticalStackLayout_0/Button_1

5.3 Child added in middle (shifts positions)

Old: Label, ButtonNew: Label, Entry, Button

ChildListChanges Label retained(0→0), Entry added(1), Button retained(1→2)

5.4 Add + remove (replace)

Old: Label, Button, EntryNew: Label, Switch, Entry

Diff: 0 node(s) with property changes, 1 child list change(s)
  children [VerticalStackLayout_0] VerticalStackLayout_0/Label_0 (unchanged), +VerticalStackLayout_0/Switch_1; removed: -VerticalStackLayout_0/Button_1

5.5 Child added with complex properties (binding)

Old: Label(A)New: Label(A), Label({Binding Name})

ChildListChanges 1 — Label retained, Label(binding) added

Diff succeeds — codegen decides how to handle the binding.


5.6 Multiple children added

Old: Label(A)New: Label(A), Button(B), Entry(C)

ChildListChanges Label retained + Button added + Entry added

5.7 Child element type swapped (at ContentPage level)

Old: <Label Text="Hello" />New: <Entry Text="Hello" />

ChildListChanges Label_0 removed, Entry_0 added

5.8 Child added (at ContentPage level)

Old: <Label Text="Hello" />New: <Label Text="Hello" /><Label Text="World" />

ChildListChanges 1 — second Label added

5.9 Child removed (at ContentPage level)

Old: <Label Text="Hello" /><Label Text="World" />New: <Label Text="Hello" />

ChildListChanges 1 — second Label removed

6 · Complex property diffs (no fallback)

6.1 Value → Binding

Old

<Label Text="Hello" />

New

<Label Text="{Binding Name}" />
Diff: 1 node(s) with property changes
  [Label_0] Text = {MarkupNode}
Property Text Set NewValue=null, NewNode=MarkupNode

6.2 Binding → Value

Old: <Label Text="{Binding Name}" /> New: <Label Text="Hello" />

Property Text Set NewValue="Hello", NewNode=null

6.3 New property as binding

Old: <Label /> New: <Label Text="{Binding Name}" />

Property Text Set NewNode=MarkupNode

6.4 Nested element property changed

Old

<Button>
    <Button.Shadow>
        <Shadow Color="Red" />
    </Button.Shadow>
</Button>

New: Shadow Color → "Blue"

NodeChanges Button_0: Shadow = {ElementNode}

6.5 Nested element property identical → empty

Same <Shadow Color="Red" /> on both sides.

Diff: empty (recursive ElementNodeEquals confirms equality)


6.6 Nested element property added

Old: <Button Text="Click" /> New: <Button Text="Click"><Button.Shadow><Shadow Color="Red" /></Button.Shadow></Button>

NodeChanges Button_0: Shadow = {ElementNode} (Set)

6.7 ListNode property identical → empty

Same <Label.GestureRecognizers> with TapGestureRecognizer + SwipeGestureRecognizer on both sides.

Diff: empty (recursive ListNodeEquals confirms equality)


6.8 ListNode property changed

Old: GestureRecognizers = [TapGestureRecognizer] New: GestureRecognizers = [TapGestureRecognizer, SwipeGestureRecognizer]

NodeChanges Label_0: GestureRecognizers = {ListNode}

6.9 Text content changed

Old: <Label>Hello</Label> New: <Label>World</Label>

Diff: 1 node(s) with property changes
  [root] _Content = "World"

6.10 Text content unchanged → empty

<Label>Hello</Label> → same

Diff: empty


7 · Multi-edit scenarios

7.1 Root property + child property

Old: Title="Page1", <Label Text="Hello" /> New: Title="Page2", <Label Text="World" />

NodeChanges 2 — root: Title="Page2", Label_0: Text="World"
ChildListChanges 0

7.2 Property change + child added

Old: VSL > Label(A) New: VSL > Label(B), Button(New)

NodeChanges 1 — Label: Text="B"
ChildListChanges 1 — Button added

7.3 Property change + child removed

Old: VSL > Label(A), Button(B) New: VSL > Label(Changed)

NodeChanges 1 — Label: Text="Changed"
ChildListChanges 1 — Button removed

7.4 Reorder + property changes on both nodes

Old: Label(A, FontSize=14), Button(B) New: Button(B2), Label(A2, FontSize=20)

NodeChanges 2 — Label: Text+FontSize, Button: Text
ChildListChanges 1 — reorder

7.5 Deep property change + child add/remove

Old

<VerticalStackLayout>
    <HorizontalStackLayout>
        <Label Text="Deep" />
    </HorizontalStackLayout>
    <Entry Text="Remove" />
</VerticalStackLayout>

New

<VerticalStackLayout>
    <HorizontalStackLayout>
        <Label Text="Changed" />
    </HorizontalStackLayout>
    <Switch />
</VerticalStackLayout>
NodeChanges 1 — VSL_0/HSL_0/Label_0: Text="Changed"
ChildListChanges 1 — Entry removed, Switch added

7.6 Property changes at multiple depths

Old: Root Title="Old", VSL Spacing="10", Label Text="A", Button Text="B" New: Root Title="New", VSL Spacing="20", Label Text="A2", Button Text="B2"

NodeChanges 4 — root, VSL_0, VSL_0/Label_0, VSL_0/HSL_1/Button_0
ChildListChanges 0

7.7 Value → Binding + child added

Old: VSL > Label(Text="Static") New: VSL > Label(Text="{Binding Name}"), Entry(Placeholder="New")

NodeChanges 1 — Label: Text = {MarkupNode}
ChildListChanges 1 — Entry added

7.8 Multi-edit debug string

Old: Title="Old", VSL > Label(A), Button(B) New: Title="New", VSL > Label(A2), Switch

Debug string contains:

  • [root] Title = "New"
  • Text = "A2"
  • + (Switch added)
  • removed: (Button removed)

8 · Sequential edits (simulating a hot reload session)

8.1 Property tweaks across 4 saves

xaml1: <Label Text="Draft"  TextColor="Gray" />
xaml2: <Label Text="Hello"  TextColor="Gray" />
xaml3: <Label Text="Hello"  TextColor="Blue" />
xaml4: <Label Text="Hello, World!" TextColor="Blue" />
Transition Diff
v1→v2 Text = "Hello"
v2→v3 TextColor = "Blue"
v3→v4 Text = "Hello, World!"

8.2 Growing layout (add children one by one)

xaml1: VSL > Label("Title")
xaml2: VSL > Label("Title"), Entry("Name")
xaml3: VSL > Label("Title"), Entry("Name"), Button("Submit")
Transition Diff
v1→v2 +Entry
v2→v3 +Button

8.3 Add → undo → modify

xaml1: VSL > Label("Hello")
xaml2: VSL > Label("Hello"), Button("Click")
xaml3: VSL > Label("Hello")                     ← undo
xaml4: VSL > Label("Goodbye")
Transition Diff
v1→v2 +Button
v2→v3 −Button
v3→v4 Text = "Goodbye" (property only)

8.4 Reorder then property change

xaml1: VSL > Label(A), Button(B), Entry(C)
xaml2: VSL > Entry(C), Label(A), Button(B)
xaml3: VSL > Entry("Search..."), Label(A), Button(B)
Transition Diff
v1→v2 Reorder (child list change, no property changes)
v2→v3 Placeholder = "Search..." (property only, no reorder)

8.5 Value → Binding → Value

xaml1: <Label Text="Static" />
xaml2: <Label Text="{Binding Name}" />
xaml3: <Label Text="Back to static" />
Transition Diff
v1→v2 Text = {MarkupNode} (NewNode set, NewValue null)
v2→v3 Text = "Back to static" (NewNode null, NewValue set)

8.6 Replace child types across edits

xaml1: VSL > Entry("Name"), Button("Submit")
xaml2: VSL > Editor("Name"), Button("Submit")
xaml3: VSL > Editor("Bio"),  ImageButton
Transition Diff
v1→v2 −Entry, +Editor
v2→v3 −Button, +ImageButton, Editor Placeholder="Bio"

9 · Edge cases

9.1 Root node has empty ID

Root property change → NodeId is "" (displayed as [root] in debug string).


9.2 Direct child ID = TypeName_index

<Label Text="A" /> → NodeId = Label_0


9.3 Nested child ID = path

<VerticalStackLayout><Label Text="A" /></VerticalStackLayout> → NodeId = VerticalStackLayout_0/Label_0


9.4 Empty tree to empty tree → empty diff

No content on either side.

Diff: empty


9.5 Single node, no children, property changed

Old: <ContentPage BackgroundColor="White"> New: <ContentPage BackgroundColor="Black">

NodeChanges root: BackgroundColor = "Black"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment