Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions Src/Common/FwAvalonia/FwAvaloniaTests/LayoutImportCoverageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,40 @@ public class ConditionalImportTests
<slice field='Gloss' editor='multistring'/>
</if>
</part>
<part id='MoInflAffixTemplate-Detail-NameAllA'>
<if field='Disabled' boolequals='true'>
<slice field='Name' label='Name' editor='multistring' ws='all analysis' menu='mnuDataTree-Help'/>
</if>
<if field='Disabled' boolequals='false'>
<slice field='Name' label='Name' editor='multistring' ws='all analysis' menu='mnuDataTree-Help'/>
</if>
</part>
<part id='MoInflAffixTemplate-Detail-Active'>
<slice field='Disabled' label='Active' editor='checkbox'/>
</part>
<part id='MoInflAffixTemplate-Detail-NestedCondition'>
<if field='Disabled' boolequals='false'>
<ifnot field='Final' boolequals='true'>
<slice field='Name' label='Name' editor='multistring' ws='all analysis'/>
</ifnot>
</if>
</part>
<part id='PartOfSpeech-Detail-SubPossibilities'>
<choice>
<where target='owner' is='CmPossibilityList' field='Depth' intlessthan='2'/>
<otherwise>
<seq field='SubPossibilities' layout='SubCategories' label='Own Label'/>
</otherwise>
</choice>
</part>
<part id='PartOfSpeech-Detail-DefaultFeatures'>
<if field='Depth' intlessthan='2'>
<obj field='DefaultFeatures' layout='Features'/>
</if>
</part>
<part id='PartOfSpeech-Detail-CategoryName'>
<slice field='Name' label='Name' editor='multistring' ws='all analysis'/>
</part>
</bin></PartInventory>";

private static ViewDefinitionModel Import(string className, string refName)
Expand All @@ -342,6 +376,200 @@ private static ViewDefinitionModel Import(string className, string refName)
$"<layout class='{className}' type='detail' name='T'><part ref='{refName}'/></layout>"), parts);
}

private static ViewDefinitionModel ImportLayout(string className, string layoutContent)
{
var parts = new DictionaryPartResolver(XElement.Parse(PartsXml));
return new XmlLayoutImporter().Import(XElement.Parse(
$"<layout class='{className}' type='detail' name='T'>{layoutContent}</layout>"), parts);
}

[Test]
public void PartRefOverrides_ReachASliceInsideEachIf()
{
var model = ImportLayout("MoInflAffixTemplate",
"<part ref='NameAllA' label='Template Name' abbr='TN' visibility='ifdata'"
+ " menu='mnuDataTree-POS-AffixTemplate' hotlinks='mnuTemplate-Hotlinks'/>");

Assert.That(model.Roots.Count, Is.EqualTo(2), "one conditional per <if>");
foreach (var conditional in model.Roots)
{
var slice = conditional.Children.Single();
Assert.That(slice.Label, Is.EqualTo("Template Name"));
Assert.That(slice.Abbreviation, Is.EqualTo("TN"));
Assert.That(slice.MenuId, Is.EqualTo("mnuDataTree-POS-AffixTemplate"));
Assert.That(slice.HotlinksId, Is.EqualTo("mnuTemplate-Hotlinks"));
Assert.That(slice.Visibility, Is.EqualTo(ViewVisibility.IfData));
}
}

[Test]
public void PartRefOverrides_ReachSlicesInsideWhereAndOtherwise()
{
var model = ImportLayout("MoAffixAllomorph",
"<part ref='AsPosition' label='Position' menu='mnuPosition'/>");

var choice = model.Roots.Single();
foreach (var branch in choice.Children)
{
var slice = branch.Children.Single();
Assert.That(slice.Label, Is.EqualTo("Position"));
Assert.That(slice.MenuId, Is.EqualTo("mnuPosition"));
}
}

[Test]
public void PartRefOverrides_ReachASliceInsideANestedConditional()
{
var model = ImportLayout("MoInflAffixTemplate",
"<part ref='NestedCondition' label='Template Name'/>");

var slice = model.Roots.Single().Children.Single().Children.Single();
Assert.That(slice.Kind, Is.EqualTo(ViewNodeKind.Field));
Assert.That(slice.Label, Is.EqualTo("Template Name"));
}

// A sequence item's heading takes its menu from the item layout's root nodes.
[Test]
public void PartRefLabelAndMenu_StayOnTheConditionalNode()
{
var model = ImportLayout("MoInflAffixTemplate",
"<part ref='NameAllA' label='Template Name' menu='mnuDataTree-POS-AffixTemplate'/>");

Assert.That(model.Roots.Select(r => r.Label), Is.All.EqualTo("Template Name"));
Assert.That(model.Roots.Select(r => r.MenuId),
Is.All.EqualTo("mnuDataTree-POS-AffixTemplate"));
}

// Field Visibility reads and sets the row's own node, so that node holds the value.
[Test]
public void PartRefVisibility_IsHeldByTheContent_NotTheConditional()
{
var template = ImportLayout("MoInflAffixTemplate",
"<part ref='NameAllA' visibility='never'/>");
foreach (var conditional in template.Roots)
{
Assert.That(conditional.Visibility, Is.EqualTo(ViewVisibility.Always));
Assert.That(conditional.Children.Single().Visibility,
Is.EqualTo(ViewVisibility.Never));
}

var choice = ImportLayout("PartOfSpeech",
"<part ref='SubPossibilities' visibility='never'/>").Roots.Single();
Assert.That(choice.Visibility, Is.EqualTo(ViewVisibility.Always));
Assert.That(choice.Children[1].Children.Single().Visibility,
Is.EqualTo(ViewVisibility.Never));
}

// Field Visibility on one branch's row reaches every branch of the same part ref.
[Test]
public void PartRefBranches_ShareOneVisibilityTarget()
{
var model = ImportLayout("MoInflAffixTemplate",
"<part ref='NameAllA'><indent><part ref='Active'/></indent></part>"
+ "<part ref='NameAllA'/>");
var first = model.Roots.Take(2).Select(c => c.Children.Single()).ToList();
var second = model.Roots.Skip(2).Select(c => c.Children.Single()).ToList();
var active = first[0].Children.Single();
Assert.That(model.Roots.Select(r => r.PartRefId), Is.All.Null);

// The applier and the canonical JSON both keep the part ref.
var hideOne = new ViewOverrideOperation(ViewOverrideOperationKind.SetVisibility,
first[1].StableId, visibility: ViewVisibility.Never);
var patched = ViewDefinitionOverrideApplier.Apply(
ViewDefinitionJsonSerializer.Deserialize(ViewDefinitionJsonSerializer.Serialize(model)),
new ViewDefinitionOverride("MoInflAffixTemplate", "T", "detail", new[] { hideOne }, null));

Assert.That(ViewDefinitionOverrideEditor.LocateTarget(patched, first[1].StableId)
.VisibilityTargets, Is.EqualTo(first.Select(s => s.StableId)));
Assert.That(ViewDefinitionOverrideEditor.LocateTarget(patched, second[0].StableId)
.VisibilityTargets, Is.EqualTo(second.Select(s => s.StableId)));
Assert.That(ViewDefinitionOverrideEditor.LocateTarget(patched, active.StableId)
.VisibilityTargets, Is.EqualTo(new[] { active.StableId }),
"the part ref's own child rows are rows of their own");
}

[Test]
public void PartRefChildren_LandUnderTheSliceInsideTheConditional()
{
var model = ImportLayout("MoInflAffixTemplate",
"<part ref='NameAllA'><indent><part ref='Active'/></indent></part>");

foreach (var conditional in model.Roots)
{
var child = conditional.Children.Single().Children.Single();
Assert.That(child.Field, Is.EqualTo("Disabled"));
Assert.That(child.Indented, Is.True);
}
}

[Test]
public void SequenceInsideAChoice_TakesTheTargetLayout_ButKeepsItsOwnLabel()
{
var model = ImportLayout("PartOfSpeech",
"<part ref='SubPossibilities' label='Subcategories' abbr='Sub' param='nested'"
+ " menu='mnuSub'/>");

var sequence = model.Roots.Single().Children[1].Children.Single();
Assert.That(sequence.Kind, Is.EqualTo(ViewNodeKind.Sequence));
Assert.That(sequence.TargetLayout, Is.EqualTo("nested"));
Assert.That(sequence.MenuId, Is.EqualTo("mnuSub"));
Assert.That(sequence.Label, Is.EqualTo("Own Label"));
Assert.That(sequence.Abbreviation, Is.Null);
}

[Test]
public void ObjectInsideAnIf_TakesTheRefsLayoutAndChildren_ButNotItsLabel()
{
var model = ImportLayout("PartOfSpeech",
"<part ref='DefaultFeatures' label='Features' param='Brief' visibility='ifdata'>"
+ "<part ref='CategoryName'/></part>");

var atom = model.Roots.Single().Children.Single();
Assert.That(atom.Kind, Is.EqualTo(ViewNodeKind.ObjectAtom));
Assert.That(atom.TargetLayout, Is.EqualTo("Brief"));
Assert.That(atom.Visibility, Is.EqualTo(ViewVisibility.IfData));
Assert.That(atom.Label, Is.Null);
Assert.That(atom.Children.Single().Field, Is.EqualTo("Name"));
}

[Test]
public void ConditionalWrittenInALayout_LeavesItsPartRefsAlone()
{
var model = ImportLayout("MoInflAffixTemplate",
"<if field='Disabled' boolequals='false'><part ref='Active'/></if>");

var conditional = model.Roots.Single();
Assert.That(conditional.Label, Is.Null);
Assert.That(conditional.Children.Single().Label, Is.EqualTo("Active"));
Assert.That(model.Diagnostics, Is.Empty);
}

[Test]
public void ChoiceWrittenInALayout_KeepsItsOwnVisibility()
{
var choice = ImportLayout("MoInflAffixTemplate",
"<choice visibility='never'><where field='Disabled' boolequals='false'>"
+ "<part ref='Active'/></where></choice>").Roots.Single();
Assert.That(choice.Kind, Is.EqualTo(ViewNodeKind.ChoiceGroup));
Assert.That(choice.Visibility, Is.EqualTo(ViewVisibility.Never));

// An <if> has no visibility of its own: the attribute is not a condition attribute.
var ifModel = ImportLayout("MoInflAffixTemplate",
"<if field='Disabled' boolequals='false' visibility='never'><part ref='Active'/></if>");
Assert.That(ifModel.Roots, Is.Empty);
Assert.That(ifModel.Diagnostics.Single().Code, Is.EqualTo("conditional-dropped"));
}

[Test]
public void PartRefDiagnostics_AreReportedOncePerConditional_NotAgainPerSlice()
{
var model = ImportLayout("MoInflAffixTemplate",
"<part ref='NameAllA' ghostInitMethod='CreateAllomorph'/>");

Assert.That(model.Diagnostics.Count(d => d.Code == "unhandled-attribute"),
Is.EqualTo(model.Roots.Count));
}

[Test]
public void If_ImportsConditionalNode_WithStructuredIntEqualsCondition()
{
Expand Down Expand Up @@ -787,6 +1015,28 @@ public void ShippedLexEntryRefNormal_ImportsTheRefTypeConditionals()
"every condition form LexEntryRef/Normal uses is in the supported set");
}

/// <summary>
/// The affix template's name part wraps its slice in a Disabled test, and its layout
/// renames the row and binds the template menu.
/// </summary>
[Test]
public void ShippedMoInflAffixTemplateDefault_NameRow_TakesTheLayoutLabelAndMenu()
{
var repoRoot = FindRepoRoot();
var (layouts, parts) = LoadShippedFiles(repoRoot);
var model = ImportShippedLayout(repoRoot, layouts, parts, "MoInflAffixTemplate", "default");

var nameRows = model.Roots
.Where(r => r.Kind == ViewNodeKind.Conditional)
.SelectMany(r => r.Children)
.Where(n => n.Field == "Name")
.ToList();
Assert.That(nameRows.Count, Is.EqualTo(2), "one per Disabled state");
Assert.That(nameRows.Select(n => n.Label), Is.All.EqualTo("Template Name"));
Assert.That(nameRows.Select(n => n.MenuId),
Is.All.EqualTo("mnuDataTree-POS-AffixTemplate"));
}

/// <summary>
/// The shipped lexeme-form ghost configuration must arrive complete on the typed
/// node -- ghost/ghostWs/ghostLabel, the explicit ghostClass (MoStemAllomorph, differing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ private static JObject WriteNode(ViewNode node)
if (node.ForceIncludeEnglish)
o["forceIncludeEnglish"] = true;
AddIfPresent(o, "visField", node.VisibilityField);
AddIfPresent(o, "partRef", node.PartRefId);
// The stored selection travels with the two facts above: options without the
// checked set would round-trip a field into the wrong checkmarks.
if (node.VisibleWritingSystems != null && node.VisibleWritingSystems.Count > 0)
Expand Down Expand Up @@ -228,6 +229,7 @@ private static ViewNode ReadNode(JToken token)
optionalWritingSystem: (string)o["optionalWs"],
forceIncludeEnglish: (bool?)o["forceIncludeEnglish"] ?? false,
visibilityField: (string)o["visField"],
partRefId: (string)o["partRef"],
visibleWritingSystems: ((JArray)o["visibleWritingSystems"])?.Select(t => (string)t).ToList());
}

Expand Down
11 changes: 10 additions & 1 deletion Src/Common/FwAvalonia/ViewDefinition/ViewDefinitionModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,10 @@ public ViewNode(
string sliceId = null,
string optionalWritingSystem = null,
bool forceIncludeEnglish = false,
string visibilityField = null)
string visibilityField = null,
string partRefId = null)
{
PartRefId = partRefId;
VisibilityField = visibilityField;
OptionalWritingSystem = optionalWritingSystem;
ForceIncludeEnglish = forceIncludeEnglish;
Expand Down Expand Up @@ -508,6 +510,13 @@ public ViewNode(
/// </summary>
public string VisibilityField { get; }

/// <summary>
/// The layout part ref this node came from, when that part's content sits inside a
/// conditional: every branch's node then holds the part ref's own values, visibility
/// included. Null on every other node.
/// </summary>
public string PartRefId { get; }

public IReadOnlyList<ViewNode> Children { get; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,10 +314,11 @@ private static ViewNode CloneWith(ViewNode n, ViewVisibility visibility, string
helpTopicId: n.HelpTopicId, sliceId: n.SliceId,
optionalWritingSystem: n.OptionalWritingSystem,
forceIncludeEnglish: n.ForceIncludeEnglish,
visibilityField: n.VisibilityField);
visibilityField: n.VisibilityField,
partRefId: n.PartRefId);

// Copy a (leaf) node under a new StableId; AutomationId is dropped so the duplicate gets a fresh,
// non-colliding identity (the renderer derives one from the new StableId by convention).
// Copy a (leaf) node under a new StableId, as a row of its own: no AutomationId
// (the renderer derives one from the new id) and no PartRefId.
private static ViewNode CloneWithId(ViewNode n, string newId)
=> new ViewNode(
newId, n.Kind, n.Label, n.Abbreviation, n.Field, n.RawEditor, n.EditorClassification,
Expand Down
Loading
Loading