Skip to content

LT-22690: Test ghost add rows, and create nothing when one is cleared - #1194

Merged
thejambi merged 4 commits into
mainfrom
LT-22690-ghost-row-tests
Oct 20, 2026
Merged

thejambi merged 4 commits into
mainfrom
LT-22690-ghost-row-tests

Conversation

@thejambi

@thejambi thejambi commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Start here: Src/xWorks/Avalonia/Composer/ComposedDetailEditContext.cs, TrySetText -- the one behavior change -- then DetailEditContextBase.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

  • Never cancelling what the row does not own. A session holding another row's edit, or one that joined an undo task opened elsewhere, is left alone and the empty text is written, as before. Pinned by ..._ClearedAfterAnotherRowsEdit_KeepsThatEdit and ..._ClearedInsideAnOuterUndoTask_KeepsNoTypedText.
  • No dead undo step. Rolling back, not deleting inside the session; pinned by Compose_GhostExample_TypedThenCleared_CreatesNothing.
  • Base-class surface. Two protected members on DetailEditContextBase; the public Stage overloads are unchanged.

Not here

  • Ghost rows the layout defines no 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.
  • The undo label still reads "Undo change to Example" (legacy: "Undo Create Example").
  • A row cleared to whitespace still counts as content, as in legacy.

Verification

  • Ghost tests: 13 passed (7 new). 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 on main too.
  • Manual, New UI: type, clear, type again, tab away -- one Example, focus stays in the row.
  • Not run: an IME keyboard pass over clear-and-retype.

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
  • Cancel the session rather than delete the created object. Deleting inside the open session leaves an undo step: LCM's UndoStack keeps a task whenever UnitOfWork.HasDataChange is true, and the owning sense counts as modified because its Examples changed. 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.
  • Ownership is tracked per session, not inferred. The view commits on every focus change, so a ghost row normally owns its session. The owner key makes that an explicit check rather than an assumption: a write under another key, or through the keyless public Stage overloads (the Reversal Entries plugin), leaves the session owned by no one, and clearing falls back to writing the empty text.
  • SubSenses ghost row kept. LexSense-Detail-Senses has no ghost= 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 rows for fields with no 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 on ghost=.
  • Undo label for a ghost creation. Would need a create-specific label when the ghost setter opens the session.
Evidence

Each new test was run against a deliberate break of the behavior it covers, and failed:

Break Failing test
Clearing never cancels TypedThenCleared_CreatesNothing
Clearing cancels any open session ClearedAfterAnotherRowsEdit_KeepsThatEdit
Commit after every keystroke CreationAndTyping_AreOneUndoStep, GhostSubsense_... (one undo step per keystroke)
Ghost setter refuses every edit every create-on-edit test
Save on every blur GhostRow_WatermarkClearsOnFocus_AndRestoresWhenLeftEmpty

The two xWorksTests failures reproduce on main with this branch's changes stashed: TextDrag_FromAvaloniaPayload_LegacyClipboardBridgeReadsBothRichAndNeutralLanes (clipboard SetDataObject fails on this machine) and BulkEditBarUiaSmokeTests.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 that
prove 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: adds protected bool IsSessionOwnedBy(string) and
    protected bool Stage(Func<bool>, string, string). The public Stage(Func<bool>)
    and Stage(Func<bool>, string) keep their signatures and behavior; writes through
    them leave the session owned by no one.
  • ComposedDetailEditContext.TrySetText: an empty value on a ghost row that owns
    the 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:

  • A cleared ghost row survived an outer undo task. When the edit session
    joined an undo task opened elsewhere, Cancel() rolled nothing back, so the
    created Example kept the text the user had cleared. (fixed during review: the
    cancel path also requires CanRollBackSession; otherwise the empty text is
    written. Pinned by Compose_GhostExample_ClearedInsideAnOuterUndoTask_KeepsNoTypedText.)
  • A rejected write dropped the ghost row's ownership. Stage changed the
    owner 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. IsNullOrEmpty means a row
    cleared to a space saves an Example containing a space.
    (Accepted for now:
    the WinForms UI behaves the same way.)
  • Undo label. A ghost creation reads "Undo change to Example"; the
    WinForms UI says "Undo Create Example".
    (Agreed to leave as is; noted in the
    PR.)
  • Owner tracking is blind to EnsureOpen callers. LexiconFirstSliceEditContext
    opens sessions through EnsureOpen directly. It has no ghost rows and never
    asks IsSessionOwnedBy, so this is harmless today.

Required Validation / Evidence

  • Ghost tests (DetailComposerTests.Compose_Ghost*): 13 passed, 7 of them new.
  • FwAvaloniaTests (full, with -CommentHygiene -TokenHygiene): 932 passed,
    1 skipped.
  • xWorksTests, with the review fixes, excluding BulkEditBarUiaSmokeTests:
    1821 of 1823 passed, 2 skipped, 0 failed. Excluded because
    FilterBar_RealizedWindow_ExposesTargetCombosThroughUIA crashes the test host on
    main too. TextDrag_FromAvaloniaPayload_LegacyClipboardBridgeReadsBothRichAndNeutralLanes
    failed in an earlier run and on main (clipboard SetDataObject), and passed in
    this one: intermittent on this machine.
  • LexiconFirstSliceEditContextEdgeCaseTests reports a fixture TearDown
    SynchronizationLockException; it reproduces on main.
  • The two review-fix tests failed before their fixes, each reproducing the
    reviewer's scenario.
  • Each original new test was shown to fail against a deliberate break of the behavior it
    covers (rollback disabled, guard removed, commit per keystroke, creation
    blocked, save on every blur), then the breaks were reverted.
  • Manual, by the author (New UI): type into the Example ghost row, clear it, type
    again, tab away: one Example created, focus stays in the row.
  • Not run: IME keyboard check of the clear-and-retype path.

Positive Observations

  • The guard never cancels another row's edit; a dedicated test pins it.
  • Clearing leaves no dead undo step: cancelling rolls the session back rather than
    deleting inside it, which LCM would keep as an undo step that changes nothing.
  • Two tests close the session before asserting, so a failure cannot leave the
    next test's setup inside an open undo task.

Interview Notes

  • Guard, in the author's words: "Clearing a ghost row will cancel the edit session
    when left empty only when it is confirmed the same edit session; another edit
    landing will not be canceled."
  • SubSenses ghost row: the layout gives it no ghost= attribute; acceptance step 5
    is read as approving it, so its test names it an approved divergence.
  • "Leaving the row empty creates nothing" was decided to include typing then
    clearing, which is stricter than the WinForms UI.
  • Whitespace-only clear: accepted, as in the WinForms UI.
  • Undo label: leave as is.
  • Nothing further to flag.

Suggested Review Focus

  • The ownership rule in DetailEditContextBase.Stage(Func<bool>, string, string).
  • Whether the SubSenses and entry Senses ghost rows should stay (approved
    divergence) or follow the layout's ghost= attributes.

🤖 Generated with Claude Code


This change is Reviewable

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>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

NUnit Tests

    1 files  ±0      1 suites  ±0   12m 55s ⏱️ + 3m 8s
6 731 tests +7  6 646 ✅ +7  85 💤 ±0  0 ❌ ±0 
6 740 runs  +7  6 655 ✅ +7  85 💤 ±0  0 ❌ ±0 

Results for commit 4c87d5a. ± Comparison against base commit 620cd99.

♻️ This comment has been updated with latest results.

@codecov-commenter

codecov-commenter commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.75%. Comparing base (620cd99) to head (4c87d5a).

Files with missing lines Patch % Lines
Src/xWorks/Avalonia/DetailEditContextBase.cs 89.47% 0 Missing and 2 partials ⚠️
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     
Files with missing lines Coverage Δ
...rks/Avalonia/Composer/ComposedDetailEditContext.cs 56.84% <100.00%> (+2.39%) ⬆️
...c/xWorks/Avalonia/Composer/LcmDetailEditSession.cs 92.59% <100.00%> (+4.13%) ⬆️
Src/xWorks/Avalonia/DetailEditContextBase.cs 76.92% <89.47%> (+2.17%) ⬆️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Zachary Burnham and others added 3 commits October 20, 2026 13:10
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
thejambi marked this pull request as ready for review October 20, 2026 19:34

@mark-sil mark-sil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@thejambi
thejambi merged commit aeaac10 into main Oct 20, 2026
8 of 9 checks passed
@thejambi
thejambi deleted the LT-22690-ghost-row-tests branch October 20, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants