Skip to content

Commit 0753090

Browse files
Min Koclaude
andcommitted
Add unit tests for cross-gallery binding warning
- CrossGalleryBinderTests: 3 tests verifying warning fires when a control outside a gallery references a control inside it, doesn't fire for same-gallery refs, and doesn't fire when gallery has no ActiveItemPropertyName - MockSymbolTable: added MockCurrentEntity support (re-implements INameResolver.CurrentEntity) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bcc4a9b commit 0753090

2 files changed

Lines changed: 257 additions & 14 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.PowerFx.Core.App.Controls;
8+
using Microsoft.PowerFx.Core.Binding;
9+
using Microsoft.PowerFx.Core.Errors;
10+
using Microsoft.PowerFx.Core.Localization;
11+
using Microsoft.PowerFx.Core.Parser;
12+
using Microsoft.PowerFx.Core.Types;
13+
using Microsoft.PowerFx.Core.Utils;
14+
using Xunit;
15+
16+
namespace Microsoft.PowerFx.Interpreter.Tests
17+
{
18+
/// <summary>
19+
/// Tests for the Binder warning that fires when a control outside a gallery
20+
/// references a control inside it (implicit cross-gallery/form reference).
21+
/// </summary>
22+
public class CrossGalleryBinderTests : IDisposable
23+
{
24+
private readonly IExternalStringResources _previousResources;
25+
26+
public CrossGalleryBinderTests()
27+
{
28+
// Save and set external string resources so the warning key resolves
29+
_previousResources = StringResources.ExternalStringResources;
30+
StringResources.ExternalStringResources = new TestStringResources();
31+
}
32+
33+
public void Dispose()
34+
{
35+
StringResources.ExternalStringResources = _previousResources;
36+
}
37+
38+
[Fact]
39+
public void CrossGalleryReference_ProducesWarning()
40+
{
41+
// Arrange: Gallery1 contains Button1 (replicable). Label1 is outside the gallery.
42+
var galleryTemplate = new DummyTemplate { ActiveItemPropertyName = "Selected" };
43+
var galleryControl = new DummyExternalControl
44+
{
45+
DisplayName = "Gallery1",
46+
Template = galleryTemplate
47+
};
48+
49+
var button1 = new DummyExternalControl
50+
{
51+
DisplayName = "Button1",
52+
IsReplicable = true,
53+
ReplicatingParent = galleryControl,
54+
Template = new DummyTemplate()
55+
};
56+
57+
var label1 = new DummyExternalControl
58+
{
59+
DisplayName = "Label1",
60+
Template = new DummyTemplate()
61+
};
62+
63+
var symbolTable = new MockSymbolTable();
64+
symbolTable.MockCurrentEntity = label1;
65+
symbolTable.AddControl("Gallery1", galleryControl, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Selected", DType.EmptyRecord, true));
66+
symbolTable.AddControl("Button1", button1, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Text", DType.String, true));
67+
symbolTable.AddControl("Label1", label1, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Text", DType.String, true));
68+
69+
var parseResult = TexlParser.ParseScript("Button1.Text");
70+
71+
// Act
72+
var binding = TexlBinding.Run(
73+
new MockGlue(),
74+
parseResult.Root,
75+
symbolTable,
76+
new BindingConfig(),
77+
DType.EmptyRecord);
78+
79+
// Assert: should produce exactly one warning about cross-gallery reference
80+
var warnings = binding.ErrorContainer.GetErrors()
81+
.Where(e => e.Severity == DocumentErrorSeverity.Warning)
82+
.ToList();
83+
Assert.Single(warnings);
84+
Assert.Equal(DocumentErrorSeverity.Warning, warnings[0].Severity);
85+
}
86+
87+
[Fact]
88+
public void SameGalleryReference_NoWarning()
89+
{
90+
// Arrange: Both Button1 and Label1 are inside the same Gallery1.
91+
var galleryTemplate = new DummyTemplate { ActiveItemPropertyName = "Selected" };
92+
var galleryControl = new DummyExternalControl
93+
{
94+
DisplayName = "Gallery1",
95+
Template = galleryTemplate
96+
};
97+
98+
var button1 = new DummyExternalControl
99+
{
100+
DisplayName = "Button1",
101+
IsReplicable = true,
102+
ReplicatingParent = galleryControl,
103+
Template = new DummyTemplate()
104+
};
105+
106+
// Label1 is also inside Gallery1 — IsDescendentOf(gallery) returns true
107+
var label1 = new GalleryChildControl
108+
{
109+
DisplayName = "Label1",
110+
IsReplicable = true,
111+
ReplicatingParent = galleryControl,
112+
ParentGallery = galleryControl,
113+
Template = new DummyTemplate()
114+
};
115+
116+
var symbolTable = new MockSymbolTable();
117+
symbolTable.MockCurrentEntity = label1;
118+
symbolTable.AddControl("Gallery1", galleryControl, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Selected", DType.EmptyRecord, true));
119+
symbolTable.AddControl("Button1", button1, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Text", DType.String, true));
120+
symbolTable.AddControl("Label1", label1, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Text", DType.String, true));
121+
122+
var parseResult = TexlParser.ParseScript("Button1.Text");
123+
124+
// Act
125+
var binding = TexlBinding.Run(
126+
new MockGlue(),
127+
parseResult.Root,
128+
symbolTable,
129+
new BindingConfig(),
130+
DType.EmptyRecord);
131+
132+
// Assert: no warning because both controls are in the same gallery
133+
var warnings = binding.ErrorContainer.GetErrors()
134+
.Where(e => e.Severity == DocumentErrorSeverity.Warning)
135+
.ToList();
136+
Assert.Empty(warnings);
137+
}
138+
139+
[Fact]
140+
public void CrossGalleryReference_NoActiveItemProperty_NoWarning()
141+
{
142+
// Arrange: Gallery has no ActiveItemPropertyName (null).
143+
var galleryTemplate = new DummyTemplate { ActiveItemPropertyName = null };
144+
var galleryControl = new DummyExternalControl
145+
{
146+
DisplayName = "Gallery1",
147+
Template = galleryTemplate
148+
};
149+
150+
var button1 = new DummyExternalControl
151+
{
152+
DisplayName = "Button1",
153+
IsReplicable = true,
154+
ReplicatingParent = galleryControl,
155+
Template = new DummyTemplate()
156+
};
157+
158+
var label1 = new DummyExternalControl
159+
{
160+
DisplayName = "Label1",
161+
Template = new DummyTemplate()
162+
};
163+
164+
var symbolTable = new MockSymbolTable();
165+
symbolTable.MockCurrentEntity = label1;
166+
symbolTable.AddControl("Gallery1", galleryControl);
167+
symbolTable.AddControl("Button1", button1, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Text", DType.String, true));
168+
symbolTable.AddControl("Label1", label1, TypeTree.Create(new List<KeyValuePair<string, DType>>()).SetItem("Text", DType.String, true));
169+
170+
var parseResult = TexlParser.ParseScript("Button1.Text");
171+
172+
// Act
173+
var binding = TexlBinding.Run(
174+
new MockGlue(),
175+
parseResult.Root,
176+
symbolTable,
177+
new BindingConfig(),
178+
DType.EmptyRecord);
179+
180+
// Assert: no warning because gallery has no ActiveItemPropertyName
181+
var warnings = binding.ErrorContainer.GetErrors()
182+
.Where(e => e.Severity == DocumentErrorSeverity.Warning)
183+
.ToList();
184+
Assert.Empty(warnings);
185+
}
186+
187+
private class GalleryChildControl : DummyExternalControl
188+
{
189+
public IExternalControl ParentGallery { get; set; }
190+
191+
public override bool IsDescendentOf(IExternalControl controlInfo)
192+
{
193+
return controlInfo == ParentGallery;
194+
}
195+
}
196+
197+
/// <summary>
198+
/// Provides the WarnImplicitGallerySelectedReference error resource for tests.
199+
/// In production, this string comes from DocServer's Resources.pares.
200+
/// </summary>
201+
private class TestStringResources : IExternalStringResources
202+
{
203+
private const string WarningMessage = "'{0}' is inside '{1}'. Use '{1}.{2}.{0}' for an explicit reference.";
204+
205+
public bool TryGet(string resourceKey, out string resourceValue, string locale = null)
206+
{
207+
if (resourceKey == "WarnImplicitGallerySelectedReference")
208+
{
209+
resourceValue = WarningMessage;
210+
return true;
211+
}
212+
213+
resourceValue = null;
214+
return false;
215+
}
216+
217+
public bool TryGetErrorResource(ErrorResourceKey resourceKey, out ErrorResource resourceValue, string locale = null)
218+
{
219+
if (resourceKey.Key == "WarnImplicitGallerySelectedReference")
220+
{
221+
var members = new Dictionary<string, Dictionary<int, string>>
222+
{
223+
{ ErrorResource.ShortMessageTag, new Dictionary<int, string> { { 1, WarningMessage } } }
224+
};
225+
resourceValue = ErrorResource.Reassemble(members);
226+
return true;
227+
}
228+
229+
resourceValue = null;
230+
return false;
231+
}
232+
}
233+
}
234+
}

src/tests/Microsoft.PowerFx.Interpreter.Tests.Shared/Helpers/SymbolTableHelperMocks/MockSymbolTable.cs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
3-
3+
44
using System.Collections.Generic;
55
using System.Linq;
6-
using Microsoft.PowerFx;
6+
using Microsoft.PowerFx;
77
using Microsoft.PowerFx.Core.App.Components;
88
using Microsoft.PowerFx.Core.App.Controls;
99
using Microsoft.PowerFx.Core.Binding;
1010
using Microsoft.PowerFx.Core.Binding.BindInfo;
11-
using Microsoft.PowerFx.Core.Functions;
11+
using Microsoft.PowerFx.Core.Entities;
12+
using Microsoft.PowerFx.Core.Functions;
1213
using Microsoft.PowerFx.Core.Glue;
1314
using Microsoft.PowerFx.Core.Types;
1415
using Microsoft.PowerFx.Core.Utils;
15-
16+
1617
/**
1718
* Just a small handy mock of symbol table to be able to customize binder to compute different token types.
1819
* Might not be 100% correct but it works and allows testing against different token types.
19-
*/
20-
internal class MockSymbolTable : ReadOnlySymbolTable
20+
*/
21+
internal class MockSymbolTable : ReadOnlySymbolTable, INameResolver
2122
{
23+
/// <summary>
24+
/// Optional: set this to simulate binding in the context of a specific control.
25+
/// Used by the Binder to determine cross-gallery references.
26+
/// </summary>
27+
public IExternalEntity MockCurrentEntity { get; set; }
28+
29+
IExternalEntity INameResolver.CurrentEntity => MockCurrentEntity;
30+
2231
public void Add(string name, NameLookupInfo info)
2332
{
2433
_variables.Add(name, info);
@@ -41,13 +50,13 @@ public void AddControlAsControlType(string name)
4150
var controlType = new DType(DKind.Control);
4251
var controlInfo = new NameLookupInfo(BindKind.Control, controlType, DPath.Root, 0);
4352
Add(name, controlInfo);
44-
}
45-
46-
public void AddControl(string name, IExternalControl dummyControl = null, TypeTree typTree = default)
47-
{
48-
var controlType = new ControlVirtualType(typTree);
49-
var controlInfo = new NameLookupInfo(BindKind.Control, controlType, DPath.Root, 0, dummyControl);
50-
Add(name, controlInfo);
53+
}
54+
55+
public void AddControl(string name, IExternalControl dummyControl = null, TypeTree typTree = default)
56+
{
57+
var controlType = new ControlVirtualType(typTree);
58+
var controlInfo = new NameLookupInfo(BindKind.Control, controlType, DPath.Root, 0, dummyControl);
59+
Add(name, controlInfo);
5160
}
5261

5362
public TypedName GetLookupInfoAsTypedName(string name)
@@ -65,4 +74,4 @@ internal override bool TryLookup(DName name, out NameLookupInfo nameInfo)
6574
{
6675
return _variables.TryGetValue(name.Value, out nameInfo);
6776
}
68-
}
77+
}

0 commit comments

Comments
 (0)