Skip to content

[patch] Fix NotImplementedException on the default render path - #104

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/record-issues-defects-n6p37v
Aug 27, 2026
Merged

[patch] Fix NotImplementedException on the default render path#104
matt-edmondson merged 1 commit into
mainfrom
claude/record-issues-defects-n6p37v

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #102. Fixes #103.

The bug

TextElement and BorderElement declared explicit switch arms that threw NotImplementedException for enum members whose behaviour the _ => fallback immediately below already implemented. An explicit arm is matched before _, so the throw won.

In every case the throwing member was the property's default value, so the failure was reached by the most ordinary use of the library — not by an exotic configuration:

Site Throwing arm Default
TextElement.CalculateHorizontalPosition HorizontalAlignment.Left HorizontalAlignment = Left
TextElement.CalculateVerticalPosition VerticalAlignment.Top VerticalAlignment = Top
BorderElement title placement HorizontalAlignment.Left TitleAlignment = Left
BorderElement.GetBorderCharacters BorderStyle.None — (enum zero value)

Both Calculate*Position methods are called unconditionally from TextElement.OnRender, so this was the main render path. Rendering a TextElement with nothing but its text set threw.

The fix

The three alignment arms now return what the fallback returned — baseX, baseY, and position.X + 1. These are pure behaviour restorations, not new logic.

BorderStyle.None needed a real decision rather than a deletion, because the _ => fallback in GetBorderCharacters is SingleLine — falling through would have silently drawn the wrong thing. OnRender now returns before drawing when the style is None, so no border and no title are drawn, while UIContainerBase still renders children afterwards and the element remains a usable container.

One deliberate non-change: BorderElement's constructor sets Padding = new Padding(1, 1, 1, 1) unconditionally, so a None border still insets its children by one cell. Making padding depend on the style is a design decision beyond this fix, and callers can already set Padding = Padding.None.

Why it shipped, and what stops it recurring

The suite was green throughout. There was no TextElementTests at all, and BorderElementTests covered only property round-tripping and invalidation — nothing called Render.

This PR adds:

  • RecordingConsoleProvider — an IConsoleProvider double that records every WriteAt call instead of drawing, so tests assert what was drawn and where. Rendering to the real SpectreConsoleProvider writes to the runner's console and leaves output unobservable.
  • TextElementTests — the full alignment matrix via [DynamicData] over Enum.GetValues, plus positional assertions (left at the content origin, right flush to the edge, centre between them), padding, word wrap, the height clamp, empty text, and invisibility.
  • BorderElementRenderTests — every BorderStyle × TitleAlignment pairing, per-style corner glyphs, the None behaviour including that children still render, the 2×2 floor, and the Width > 4 title threshold.

Driving the matrices from Enum.GetValues rather than hardcoded lists means a newly added enum member is covered automatically instead of silently skipped — which is the specific shape of the original miss.

Verification

113 tests pass, up from 71.

Mutation-checked: reintroducing the three alignment throws by substitution fails 21 of 113 tests, so the new coverage is load-bearing rather than incidental. The tree was restored from backups and re-verified afterwards (grep for NotImplementedException across TUI.Core returns nothing but build artifacts, and the diff is only the intended four changes).

Also confirmed end-to-end against a standalone harness compiling the real TUI.Core sources: all 28 alignment/style combinations render, where previously the default configuration threw.

Note for reviewers

CLAUDE.md gains a Render tests subsection covering the recorder, the enum-matrix convention, and the two sizing traps that bite when writing these (an element smaller than its own padding has an empty content area and draws nothing; BorderElement suppresses its title below Width > 4). The second one cost me a red test while writing this.

I could not run the repo's own analyzers locally — this sandbox's SDK is 10.0.111 (Roslyn 5.0.0.0) and ktsu.Sdk.Analyzers 2.28.0 requires 5.9.0.0, so CSC refuses to load it with CS9057. The .NET analyzers and EnforceCodeStyleInBuild did run and are clean (two IDE violations were caught and fixed that way). CI is the first place the ktsu analyzers will actually run against this change.


Generated by Claude Code

TextElement and BorderElement declared explicit switch arms that threw
NotImplementedException for enum members whose behaviour the `_` fallback
immediately below already implemented. An explicit arm is matched before `_`,
so the throw won.

In every case the throwing member was the property's default value, so the
failure was reached by the most ordinary use of the library:

- TextElement.HorizontalAlignment defaults to Left, which threw.
- TextElement.VerticalAlignment defaults to Top, which threw.
- BorderElement.TitleAlignment defaults to Left, so any titled border threw.
- BorderStyle.None threw instead of drawing no border.

CalculateHorizontalPosition and CalculateVerticalPosition are called
unconditionally from TextElement.OnRender, so this was the main render path
rather than a rare branch. Rendering a TextElement with nothing but its text
set threw.

The three alignment arms now return what the fallback returned. BorderStyle.None
gets a real implementation rather than the fallback's single-line border:
OnRender returns before drawing, so no border and no title are drawn, while the
base class still renders children and the element stays a usable container.

Tests: the suite was green throughout because nothing called Render. There was
no TextElementTests at all, and BorderElementTests covered only property
round-tripping and invalidation. Adds RecordingConsoleProvider, an
IConsoleProvider double that records WriteAt calls so tests can assert what was
drawn and where, plus TextElementTests and BorderElementRenderTests covering the
full alignment and border-style matrix via [DynamicData] over Enum.GetValues.

Verified by mutation: reintroducing the three throws fails 21 of the 113 tests.

Fixes #102
Fixes #103

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTHNXgSNEHUSQ5KMLgivno
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 67b237a into main Aug 27, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/record-issues-defects-n6p37v branch August 27, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants