@@ -1555,6 +1555,203 @@ public void Compose_GhostWithoutGhostField_IsNotEditable_SoTypingCannotCreateAnd
15551555 "no bare object was created behind the user's back" ) ;
15561556 }
15571557
1558+ private static DetailField ExampleGhost ( ComposedDetail composed , ILexSense sense )
1559+ => composed . Model . Fields . FirstOrDefault ( f => f . Field == "Examples"
1560+ && f . StableId . EndsWith ( DetailField . GhostStableIdSuffix ) && f . ObjectHvo == sense . Hvo ) ;
1561+
1562+ // The layout gives SubSenses no ghost= attribute; LT-22690 approves its ghost row.
1563+ private static DetailField SubsenseGhost ( ComposedDetail composed , ILexSense sense )
1564+ => composed . Model . Fields . FirstOrDefault ( f => f . Field == "Senses"
1565+ && f . StableId . EndsWith ( DetailField . GhostStableIdSuffix ) && f . ObjectHvo == sense . Hvo ) ;
1566+
1567+ // One stage per keystroke, as the view does.
1568+ private static void TypeInto ( ComposedDetail composed , DetailField field , string text )
1569+ {
1570+ for ( var length = 1 ; length <= text . Length ; length ++ )
1571+ {
1572+ Assert . That ( composed . EditContext . TrySetText ( field , field . Values [ 0 ] . WsAbbrev ,
1573+ text . Substring ( 0 , length ) ) , Is . True , "each keystroke stages" ) ;
1574+ }
1575+ }
1576+
1577+ [ Test ]
1578+ public void Compose_GhostExample_TypingCreatesTheExample_AndRecomposesIntoItsRows ( )
1579+ {
1580+ var sense = m_entry . SensesOS [ 0 ] ;
1581+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1582+ var ghost = ExampleGhost ( composed , sense ) ;
1583+ Assert . That ( ghost , Is . Not . Null , "a sense with no examples shows the Example ghost row" ) ;
1584+ Assert . That ( ghost . IsEditable , Is . True ) ;
1585+ Assert . That ( ghost . GhostPrompt , Does . Contain ( "Example" ) ) ;
1586+
1587+ TypeInto ( composed , ghost , "una casa" ) ;
1588+ composed . EditContext . Commit ( ) ;
1589+
1590+ Assert . That ( sense . ExamplesOS . Count , Is . EqualTo ( 1 ) , "typing created the example" ) ;
1591+ var example = sense . ExamplesOS [ 0 ] ;
1592+ Assert . That ( example . Example . get_String ( Cache . DefaultVernWs ) . Text , Is . EqualTo ( "una casa" ) ,
1593+ "the typed text landed in the Example field, in the vernacular writing system" ) ;
1594+
1595+ var recomposed = DetailComposer . Compose ( m_entry , Cache ) ;
1596+ Assert . That ( ExampleGhost ( recomposed , sense ) , Is . Null ,
1597+ "with an example present the ghost row gives way to the example's own rows" ) ;
1598+ var exampleRow = recomposed . Model . Fields . FirstOrDefault ( f =>
1599+ f . Field == "Example" && f . ObjectHvo == example . Hvo ) ;
1600+ Assert . That ( exampleRow , Is . Not . Null , "the new example composes its Example row" ) ;
1601+ Assert . That ( exampleRow . Values . Any ( v => v . Value == "una casa" ) , Is . True ,
1602+ "the Example row shows the typed text" ) ;
1603+ }
1604+
1605+ [ Test ]
1606+ public void Compose_GhostExample_CreationAndTyping_AreOneUndoStep ( )
1607+ {
1608+ var sense = m_entry . SensesOS [ 0 ] ;
1609+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1610+ var stepsBefore = Cache . ActionHandlerAccessor . UndoableSequenceCount ;
1611+
1612+ TypeInto ( composed , ExampleGhost ( composed , sense ) , "una casa" ) ;
1613+ composed . EditContext . Commit ( ) ;
1614+ Assert . That ( Cache . ActionHandlerAccessor . UndoableSequenceCount , Is . EqualTo ( stepsBefore + 1 ) ,
1615+ "creating the example and every keystroke typed into it are one undo step" ) ;
1616+
1617+ Cache . ActionHandlerAccessor . Undo ( ) ;
1618+ Assert . That ( sense . ExamplesOS . Count , Is . Zero , "one undo removes the example" ) ;
1619+ Assert . That ( ExampleGhost ( DetailComposer . Compose ( m_entry , Cache ) , sense ) , Is . Not . Null ,
1620+ "after the undo the sense shows its Example ghost row again" ) ;
1621+ }
1622+
1623+ [ Test ]
1624+ public void Compose_GhostExample_TypedThenCleared_CreatesNothing ( )
1625+ {
1626+ var sense = m_entry . SensesOS [ 0 ] ;
1627+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1628+ var ghost = ExampleGhost ( composed , sense ) ;
1629+ var stepsBefore = Cache . ActionHandlerAccessor . UndoableSequenceCount ;
1630+
1631+ TypeInto ( composed , ghost , "un" ) ;
1632+ Assert . That ( sense . ExamplesOS . Count , Is . EqualTo ( 1 ) , "the first keystroke creates the example" ) ;
1633+ Assert . That ( composed . EditContext . TrySetText ( ghost , ghost . Values [ 0 ] . WsAbbrev , "u" ) , Is . True ) ;
1634+ Assert . That ( composed . EditContext . TrySetText ( ghost , ghost . Values [ 0 ] . WsAbbrev , "" ) , Is . True ,
1635+ "clearing the row is accepted" ) ;
1636+ var sessionOpenAfterClear = composed . EditContext . IsOpen ;
1637+ var examplesAfterClear = sense . ExamplesOS . Count ;
1638+ // Close the session first, so a failure cannot break the next test's setup.
1639+ composed . EditContext . Commit ( ) ;
1640+
1641+ Assert . That ( sessionOpenAfterClear , Is . False , "clearing the row ends its edit session" ) ;
1642+ Assert . That ( examplesAfterClear , Is . Zero , "clearing the row removes the example it created" ) ;
1643+ Assert . That ( Cache . ActionHandlerAccessor . UndoableSequenceCount , Is . EqualTo ( stepsBefore ) ,
1644+ "nothing reaches the undo stack, not even a step that changes nothing" ) ;
1645+
1646+ TypeInto ( composed , ghost , "b" ) ;
1647+ composed . EditContext . Commit ( ) ;
1648+ Assert . That ( sense . ExamplesOS . Count , Is . EqualTo ( 1 ) , "typing again creates the example afresh" ) ;
1649+ Assert . That ( sense . ExamplesOS [ 0 ] . Example . get_String ( Cache . DefaultVernWs ) . Text , Is . EqualTo ( "b" ) ) ;
1650+ }
1651+
1652+ [ Test ]
1653+ public void Compose_GhostExample_ClearedAfterAnotherRowsEdit_KeepsThatEdit ( )
1654+ {
1655+ var sense = m_entry . SensesOS [ 0 ] ;
1656+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1657+ var gloss = composed . Model . Fields . First ( f => f . Field == "Gloss" && f . ObjectHvo == sense . Hvo ) ;
1658+ var ghost = ExampleGhost ( composed , sense ) ;
1659+
1660+ var glossStaged = composed . EditContext . TrySetText ( gloss , gloss . Values [ 0 ] . WsAbbrev , "home" ) ;
1661+ var typedStaged = composed . EditContext . TrySetText ( ghost , ghost . Values [ 0 ] . WsAbbrev , "u" ) ;
1662+ var clearStaged = composed . EditContext . TrySetText ( ghost , ghost . Values [ 0 ] . WsAbbrev , "" ) ;
1663+ var sessionOpenAfterClear = composed . EditContext . IsOpen ;
1664+ // Close the session first, so a failure cannot break the next test's setup.
1665+ composed . EditContext . Commit ( ) ;
1666+
1667+ Assert . That ( glossStaged && typedStaged && clearStaged , Is . True , "every edit stages" ) ;
1668+ Assert . That ( sessionOpenAfterClear , Is . True ,
1669+ "the session holds another row's edit, so clearing the ghost row does not cancel it" ) ;
1670+ Assert . That ( sense . Gloss . get_String ( Cache . DefaultAnalWs ) . Text , Is . EqualTo ( "home" ) ,
1671+ "the other row's edit is saved" ) ;
1672+ }
1673+
1674+ [ Test ]
1675+ public void Compose_GhostExample_ClearedAfterAnotherRowsRejectedEdit_CreatesNothing ( )
1676+ {
1677+ var sense = m_entry . SensesOS [ 0 ] ;
1678+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1679+ var gloss = composed . Model . Fields . First ( f => f . Field == "Gloss" && f . ObjectHvo == sense . Hvo ) ;
1680+ var ghost = ExampleGhost ( composed , sense ) ;
1681+
1682+ TypeInto ( composed , ghost , "u" ) ;
1683+ var glossStaged = composed . EditContext . TrySetText ( gloss , "no-such-ws" , "home" ) ;
1684+ var clearStaged = composed . EditContext . TrySetText ( ghost , ghost . Values [ 0 ] . WsAbbrev , "" ) ;
1685+ var sessionOpenAfterClear = composed . EditContext . IsOpen ;
1686+ var examplesAfterClear = sense . ExamplesOS . Count ;
1687+ // Close the session first, so a failure cannot break the next test's setup.
1688+ composed . EditContext . Commit ( ) ;
1689+
1690+ Assert . That ( glossStaged , Is . False , "precondition: the gloss row rejects an unknown writing system" ) ;
1691+ Assert . That ( clearStaged , Is . True ) ;
1692+ Assert . That ( sessionOpenAfterClear , Is . False ,
1693+ "a rejected edit wrote nothing, so the session still holds only the ghost row's edits" ) ;
1694+ Assert . That ( examplesAfterClear , Is . Zero , "clearing the row removes the example it created" ) ;
1695+ }
1696+
1697+ // An undo task opened before the row is typed into belongs to its opener, and cancelling
1698+ // the edit session cannot roll it back.
1699+ [ Test ]
1700+ public void Compose_GhostExample_ClearedInsideAnOuterUndoTask_KeepsNoTypedText ( )
1701+ {
1702+ var sense = m_entry . SensesOS [ 0 ] ;
1703+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1704+ var ghost = ExampleGhost ( composed , sense ) ;
1705+
1706+ Cache . DomainDataByFlid . BeginUndoTask ( "outer" , "outer" ) ;
1707+ try
1708+ {
1709+ TypeInto ( composed , ghost , "u" ) ;
1710+ Assert . That ( composed . EditContext . TrySetText ( ghost , ghost . Values [ 0 ] . WsAbbrev , "" ) , Is . True ,
1711+ "clearing the row is accepted" ) ;
1712+ composed . EditContext . Commit ( ) ;
1713+ }
1714+ finally
1715+ {
1716+ Cache . DomainDataByFlid . EndUndoTask ( ) ;
1717+ }
1718+
1719+ Assert . That ( sense . ExamplesOS . Select ( e => e . Example . get_String ( Cache . DefaultVernWs ) . Text ?? "" ) ,
1720+ Has . All . Empty , "the text the user cleared is not saved by the outer task" ) ;
1721+ }
1722+
1723+ [ Test ]
1724+ public void Compose_GhostSubsense_TypingCreatesASubsense_WithTheTypedGloss ( )
1725+ {
1726+ var sense = m_entry . SensesOS [ 0 ] ;
1727+ var composed = DetailComposer . Compose ( m_entry , Cache ) ;
1728+ var ghost = SubsenseGhost ( composed , sense ) ;
1729+ Assert . That ( ghost , Is . Not . Null , "a sense with no subsenses shows the SubSenses ghost row" ) ;
1730+ Assert . That ( ghost . IsEditable , Is . True ) ;
1731+ var stepsBefore = Cache . ActionHandlerAccessor . UndoableSequenceCount ;
1732+
1733+ TypeInto ( composed , ghost , "small house" ) ;
1734+ composed . EditContext . Commit ( ) ;
1735+
1736+ Assert . That ( sense . SensesOS . Count , Is . EqualTo ( 1 ) , "typing created the subsense" ) ;
1737+ var subsense = sense . SensesOS [ 0 ] ;
1738+ Assert . That ( subsense . Gloss . get_String ( Cache . DefaultAnalWs ) . Text , Is . EqualTo ( "small house" ) ,
1739+ "the typed text became the subsense's gloss" ) ;
1740+ Assert . That ( Cache . ActionHandlerAccessor . UndoableSequenceCount , Is . EqualTo ( stepsBefore + 1 ) ,
1741+ "creating the subsense and typing its gloss are one undo step" ) ;
1742+
1743+ var recomposed = DetailComposer . Compose ( m_entry , Cache ) ;
1744+ Assert . That ( SubsenseGhost ( recomposed , sense ) , Is . Null ,
1745+ "with a subsense present the ghost row gives way to the subsense's own rows" ) ;
1746+ Assert . That ( recomposed . Model . Fields . Any ( f => f . Field == "Gloss" && f . ObjectHvo == subsense . Hvo ) ,
1747+ Is . True , "the new subsense composes its Gloss row" ) ;
1748+
1749+ Cache . ActionHandlerAccessor . Undo ( ) ;
1750+ Assert . That ( sense . SensesOS . Count , Is . Zero , "one undo removes the subsense" ) ;
1751+ Assert . That ( SubsenseGhost ( DetailComposer . Compose ( m_entry , Cache ) , sense ) , Is . Not . Null ,
1752+ "after the undo the sense shows its SubSenses ghost row again" ) ;
1753+ }
1754+
15581755 // Conditional display: the real shipped variant/complex-form
15591756 // divergence. LexEntryRef/Normal's VariantEntryTypes and ComplexEntryTypes parts are
15601757 // <if field="RefType" intequals="0|1"> twins (LexEntryParts.xml:1133-1162); exactly one may
0 commit comments