Repository navigation
LT-22690: Test ghost add rows, and create nothing when one is cleared - #1194
Merged
Merged
Conversation
Typing into a ghost row creates its object on the first keystroke, so typing and then clearing the row before leaving it saved an empty object. The edit context now records which row opened the edit session. Clearing a ghost row whose edits are the whole session cancels that session: nothing is created and no undo step remains. An edit another row staged into the same session is never cancelled. New tests cover the ticket's acceptance steps. Typing into the Example and SubSenses ghost rows creates the object and the view re-composes into its rows. Creating and typing are one undo step, and undoing it brings the ghost row back. Focusing and leaving an untouched ghost row stages and saves nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1194 +/- ##
=======================================
Coverage 39.74% 39.75%
=======================================
Files 1544 1544
Lines 355085 355109 +24
Branches 41247 41254 +7
=======================================
+ Hits 141121 141158 +37
+ Misses 184600 184591 -9
+ Partials 29364 29360 -4
🚀 New features to boost your workflow:
|
Shorten the session-ownership doc comments to their contracts, and drop test comments that restate the helper or test name. Comment-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two gaps in clearing a ghost row before leaving it: - When the edit session joined an undo task opened elsewhere, cancelling it rolled nothing back, so the created Example kept the text the user had just cleared. The cancel path now also requires a session that can roll back; otherwise the empty text is written. - A rejected write from another row dropped the ghost row's ownership of the session. Ownership now changes only on a successful write, and is cleared when a setter throws in an already-open session. A test for each reproduces the gap and passes with its fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thejambi
marked this pull request as ready for review
October 20, 2026 19:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Start here:
Src/xWorks/Avalonia/Composer/ComposedDetailEditContext.cs,TrySetText-- the one behavior change -- thenDetailEditContextBase.Stage(Func<bool>, string, string), the rule it relies on.LT-22690 audits the New UI detail view's ghost add rows ("Type to add Example") against the legacy ghost-slice create-on-edit path. The acceptance test already passes on
main, so this is mostly tests that prove it: typing into the Example and SubSenses ghost rows creates the object and the view re-composes into its rows; creation plus typing is one undo step, and undoing it brings the ghost row back; focusing and leaving an untouched ghost row stages nothing.One behavior is new. Typing into a ghost row creates its object on the first keystroke, so typing and then clearing the row before leaving it saved an empty Example -- as legacy does. Now clearing it creates nothing. The edit context records which row opened the edit session; a ghost row that clears while its own edits are the whole session cancels the session, which rolls back the creation and leaves no undo step.
Where to look
..._ClearedAfterAnotherRowsEdit_KeepsThatEditand..._ClearedInsideAnOuterUndoTask_KeepsNoTypedText.Compose_GhostExample_TypedThenCleared_CreatesNothing.protectedmembers onDetailEditContextBase; the publicStageoverloads are unchanged.Not here
ghost=for: SubSenses and entry Senses are kept as an approved divergence (acceptance step 5); the non-editable "Type to add AlternateForms" rows are left for a follow-up.Verification
FwAvaloniaTests: 932 passed, 1 skipped. Both hygiene checks clean.xWorksTests: 1821 of 1823 passed, 2 skipped, with the BulkEditBar UIA fixture excluded -- it crashes the test host onmaintoo.Next: review the ownership rule in
Stage, or tell me the clear-to-cancel behavior belongs in its own ticket.Reading this a year from now -- start here
This PR is the record of LT-22690's audit and its one behavior decision. The ticket's acceptance test passed by hand before any change; sprint planning scoped the work to tests. "Leaving the row empty creates nothing" was then read to include typing and clearing before leaving, which is stricter than the legacy ghost slice, and implemented here.
Decisions, and why
UndoStackkeeps a task wheneverUnitOfWork.HasDataChangeis true, and the owning sense counts as modified because itsExampleschanged. The Edit menu would offer an "Undo change to Example" that does nothing, and the sense's DateModified would move. Cancelling rolls both the creation and the text back.Stageoverloads (the Reversal Entries plugin), leaves the session owned by no one, and clearing falls back to writing the empty text.LexSense-Detail-Senseshas noghost=attribute, so legacy shows no SubSenses ghost row. Acceptance step 5 asks for one; it is read as an approved divergence, and its test says so.Deferred, and what would unblock it
ghost=. The composer emits a prompt row for every empty always-visible owning field. Under Variants, Allomorphs and Grammatical Info. Details these are non-editable and labelled with the raw field name ("Type to add AlternateForms"), and neither label nor prompt goes through StringTable. Needs a decision on gating ghost emission onghost=.Evidence
Each new test was run against a deliberate break of the behavior it covers, and failed:
TypedThenCleared_CreatesNothingClearedAfterAnotherRowsEdit_KeepsThatEditCreationAndTyping_AreOneUndoStep,GhostSubsense_...(one undo step per keystroke)GhostRow_WatermarkClearsOnFocus_AndRestoresWhenLeftEmptyThe two
xWorksTestsfailures reproduce onmainwith this branch's changes stashed:TextDrag_FromAvaloniaPayload_LegacyClipboardBridgeReadsBothRichAndNeutralLanes(clipboardSetDataObjectfails on this machine) andBulkEditBarUiaSmokeTests.FilterBar_RealizedWindow_ExposesTargetCombosThroughUIA(test host crash).Preflight review details
Code Review Summary
Branch: LT-22690-ghost-row-tests
Base: origin/main (620cd99)
Date: 2026-10-20
Review model: Claude Opus 5.5
Files changed: 4
Overview
LT-22690 audits the New UI (Avalonia) detail view's ghost add rows ("Type to add
X") against the WinForms ghost-slice create-on-edit path. The acceptance test
already passed by hand on
main; sprint planning scoped the work to tests thatprove it. One behavior was added on the author's decision: typing into a ghost
row and clearing it before leaving the field creates nothing.
Tests cover the acceptance steps: typing into the Example and SubSenses ghost
rows creates the object and the view re-composes into its rows; creation plus
typing is one undo step that brings the ghost row back when undone; focusing and
leaving an untouched ghost row stages nothing. The clear-before-leaving behavior
cancels the edit session when it holds only that ghost row's edits, so nothing
is created and no undo step remains.
Contract/API Changes
DetailEditContextBase: addsprotected bool IsSessionOwnedBy(string)andprotected bool Stage(Func<bool>, string, string). The publicStage(Func<bool>)and
Stage(Func<bool>, string)keep their signatures and behavior; writes throughthem leave the session owned by no one.
ComposedDetailEditContext.TrySetText: an empty value on a ghost row that ownsthe open session now cancels the session instead of writing the empty string.
Findings
Critical - Must address before merge
None.
Important - Should address before merge
Raised by an external review after the PR was opened:
joined an undo task opened elsewhere,
Cancel()rolled nothing back, so thecreated Example kept the text the user had cleared. (fixed during review: the
cancel path also requires
CanRollBackSession; otherwise the empty text iswritten. Pinned by
Compose_GhostExample_ClearedInsideAnOuterUndoTask_KeepsNoTypedText.)Stagechanged theowner before the setter ran, so another row's rejected write left the session
ownerless. (fixed during review: ownership changes only on a successful write,
and is cleared when a setter throws in an already-open session. Pinned by
Compose_GhostExample_ClearedAfterAnotherRowsRejectedEdit_CreatesNothing.)Minor - Consider
Whitespace-only clear is treated as content.(Accepted for now:IsNullOrEmptymeans a rowcleared to a space saves an Example containing a space.
the WinForms UI behaves the same way.)
Undo label. A ghost creation reads "Undo change to Example"; the(Agreed to leave as is; noted in theWinForms UI says "Undo Create Example".
PR.)
EnsureOpencallers.LexiconFirstSliceEditContextopens sessions through
EnsureOpendirectly. It has no ghost rows and neverasks
IsSessionOwnedBy, so this is harmless today.Required Validation / Evidence
DetailComposerTests.Compose_Ghost*): 13 passed, 7 of them new.FwAvaloniaTests(full, with-CommentHygiene -TokenHygiene): 932 passed,1 skipped.
xWorksTests, with the review fixes, excludingBulkEditBarUiaSmokeTests:1821 of 1823 passed, 2 skipped, 0 failed. Excluded because
FilterBar_RealizedWindow_ExposesTargetCombosThroughUIAcrashes the test host onmaintoo.TextDrag_FromAvaloniaPayload_LegacyClipboardBridgeReadsBothRichAndNeutralLanesfailed in an earlier run and on
main(clipboardSetDataObject), and passed inthis one: intermittent on this machine.
LexiconFirstSliceEditContextEdgeCaseTestsreports a fixture TearDownSynchronizationLockException; it reproduces onmain.reviewer's scenario.
covers (rollback disabled, guard removed, commit per keystroke, creation
blocked, save on every blur), then the breaks were reverted.
again, tab away: one Example created, focus stays in the row.
Positive Observations
deleting inside it, which LCM would keep as an undo step that changes nothing.
next test's setup inside an open undo task.
Interview Notes
when left empty only when it is confirmed the same edit session; another edit
landing will not be canceled."
ghost=attribute; acceptance step 5is read as approving it, so its test names it an approved divergence.
clearing, which is stricter than the WinForms UI.
Suggested Review Focus
DetailEditContextBase.Stage(Func<bool>, string, string).divergence) or follow the layout's
ghost=attributes.🤖 Generated with Claude Code
This change is