writing-unit-tests
Skill181 starsChanged 7 months agoMITView on GitHub
Guidelines for writing unit tests in the Hex1b TUI library. Use when creating new tests for widgets, nodes, or terminal functionality.
We split this skill into the rules it gives an agent
24 rules. 0 are common, 10 are distinctive, and 14 are very specific to this project.
0
Common
Most of the 4 edited versions we read keep these.
10
Distinctive
This author's own choices. Worth a look.
14
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 12
Prefer full terminal stack testing: - Use Hex1bTerminal.CreateBuilder() to create complete terminal environments
- Distinctive: line 14
Keep tests simple and linear: - Avoid excessive abstractions.
- Very specific: line 15
Assert on visual behavior: - Use CellPatternSearcher and color assertions for render verification
- Distinctive: line 16
Update this skill: when discovering new patterns - Build the body of knowledge as part of PRs
- Very specific: line 33
Test files import Microsoft.VisualStudio.TestTools.UnitTesting.
- Very specific: line 75
.WithHeadless(): - No actual terminal output (CI-safe) - Very specific: line 222
Guideline: If you're going to assert on specific screen content, include it in the WaitUntil condition.
- Very specific: line 253
Fix: Always add WaitUntil after any action that changes state:
- Very specific: line 270
Fix: Use TaskCompletionSource to signal completion:
- Very specific: line 299
Use queued in-memory presentation reads with Hex1bAppWorkloadAdapter.
- Very specific: line 326
Keep the application owned by the fixture before awaiting startup so cleanup can dispose it even if startup fails.
- Very specific: line 349
Check input ordering with coalescing both enabled and disabled.
- Very specific: line 376
Use /bin/sh with stty -echo, a newline-free readiness marker, and input-gated output to test the real PTY without interactive shell startup …
- Distinctive: line 404
Test each endpoint against a scripted peer with pinned numeric frame types, little-endian headers, and literal JSON from the pre-change …
- Distinctive: line 422
When writing tests for widgets, consider all the dimensions that affect behavior.
- Distinctive: line 513
Verify that widgets respect theme colors and can be customized:
- Distinctive: line 573
Default theme renders correctly
- Distinctive: line 591
Not every widget needs every combination, but consider which dimensions are relevant for the widget's behavior.
- Very specific: line 605
Keep parsing and HWT frame generation outside the measured region, then separately verify frame delivery and batch accounting.
- Very specific: line 612
Use literal expected bytes independent of the production key/text mapper.
- Very specific: line 635
Run native console tests in a child process under WindowsProxyPtyHandle, not against the test runner's own console.
- Distinctive: line 642
Do not synchronize on the outgoing KGP query: some ConPTY hosts consume APC queries instead of forwarding them, even though input can still …
- Distinctive: line 644
Keep the existing test-host packaging unchanged rather than changing the host for unrelated PTY tests to satisfy this fixture.
- Distinctive: line 705
When you discover a new testing pattern while writing tests:
“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing Unit Tests Skill
This skill provides guidelines for AI agents writing unit tests for the Hex1b TUI library. It outlines the preferred testing approach, patterns, and anti-patterns to avoid. Tests use MSTest 4 with MSTest.Sdk/4.2.3, OutputType=Exe, and Microsoft.Testing.Platform (MTP). global.json configures dotnet test to use MTP; test projects can also run in executable mode with dotnet run --project tests/SomeProject/.
Core Philosophy
- Prefer full terminal stack testing - Use
Hex1bTerminal.CreateBuilder()to create complete terminal environments - Use
.WithHex1bApp()for TUI functionality tests - This wires up the full app lifecycle - Keep tests simple and linear - Avoid excessive abstractions; repeating patterns are beneficial for AI agents
- Assert on visual behavior - Use
CellPatternSearcherand color assertions for render verification - Update this skill when discovering new patterns - Build the body of knowledge as part of PRs
When to Use Full Stack vs Isolation
| Test Type | Approach |
| Widget behavior, layout, rendering | Full stack with Hex1bTerminal.CreateBuilder() |
| Input handling, focus navigation | Full stack with WithHex1bApp() |
| Low-level APIs (Surface, SurfaceCell) | Test in isolation (dependencies of Hex1bApp) |
| Color/theme verification | Full stack with snapshot color assertions |
Standard Test Structure
Full Stack Integration Test
Test files import Microsoft.VisualStudio.TestTools.UnitTesting. The Hex1b.Testing namespace is global-using'd via Directory.Build.props for helpers such as TestSeq. For test output, add public TestContext TestContext { get; set; } = null!; and call TestContext.WriteLine(...). Suppressed MSTest analyzers are MSTEST0014, MSTEST0030, MSTEST0032, and MSTEST0057.
This is the preferred pattern for most tests:
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class WidgetNameTests
{
[TestMethod]
public async Task WidgetName_Scenario_ExpectedBehavior()
{
// Arrange - Build the terminal with the app
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => new VStackWidget([
new TextBlockWidget("Hello"),
new ButtonWidget("Click Me")
]))
.WithHeadless()
.WithDimensions(80, 24)
.Build();
// Act & Assert - Use input sequencer with WaitUntil
var snapshot = await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Hello"), TimeSpan.FromSeconds(2), "initial render")
.Down() // Navigate to button
.WaitUntil(s => s.ContainsText("> Click Me"), TimeSpan.FromSeconds(2), "button focused")
.Capture("focused-button")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
// Assert (often redundant if WaitUntil already verified)
Assert.IsTrue(snapshot.ContainsText("> Click Me"));
}
}Key Elements
await using var terminal- Ensures proper disposal.WithHeadless()- No actual terminal output (CI-safe).WithDimensions(80, 24)- Explicit terminal sizeWaitUntilbefore assertions - Prevents timing issues.Capture("name")- Saves SVG/HTML for debuggingCtrl().Key(Hex1bKey.C)- Clean exit
Input Sequencing Patterns
Basic Navigation
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Item 1"), TimeSpan.FromSeconds(2), "list rendered")
.Down()
.WaitUntil(s => s.ContainsText("> Item 2"), TimeSpan.FromSeconds(2), "moved to item 2")
.Down()
.WaitUntil(s => s.ContainsText("> Item 3"), TimeSpan.FromSeconds(2), "moved to item 3")
.Capture("navigation-result")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);Text Input
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Name:"), TimeSpan.FromSeconds(2), "form rendered")
.Type("John Doe")
.WaitUntil(s => s.ContainsText("John Doe"), TimeSpan.FromSeconds(2), "text entered")
.Capture("text-input")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);Keyboard Shortcuts
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Ready"), TimeSpan.FromSeconds(2), "app ready")
.Ctrl().Key(Hex1bKey.S) // Ctrl+S
.WaitUntil(s => s.ContainsText("Saved"), TimeSpan.FromSeconds(2), "save completed")
.Capture("after-save")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);Visual Assertion Patterns
Using CellPatternSearcher
For precise cell-level assertions:
// Find a specific character
var pattern = new CellPatternSearcher().Find('█');
var result = pattern.Search(snapshot);
Assert.IsTrue(result.HasMatches);
Assert.AreEqual(expectedX, result.First!.Start.X);
// Find with regex pattern
var pattern = new CellPatternSearcher().FindPattern(@"Count:\s*\d+");
var result = pattern.Search(snapshot);
Assert.IsTrue(result.HasMatches);
// Find with predicate
var pattern = new CellPatternSearcher()
.Find(ctx => char.IsDigit(ctx.Cell.Character[0]));
var result = pattern.Search(snapshot);
Assert.AreEqual(3, result.Count);Color Assertions
For verifying themed/styled output:
// Check if any cell has a specific background color
Assert.IsTrue(snapshot.HasBackgroundColor(Hex1bColor.FromRgb(0, 100, 200)),
"Button should have blue background");
// Check if any cell has a specific foreground color
Assert.IsTrue(snapshot.HasForegroundColor(Hex1bColor.FromRgb(255, 255, 255)),
"Text should be white");
// Get color at specific position
var bgColor = snapshot.GetBackgroundColor(10, 5);
Assert.AreEqual(Hex1bColor.FromRgb(255, 0, 0), bgColor);
// Check uniform row background
Assert.IsTrue(snapshot.HasUniformBackgroundColor(0, Hex1bColor.FromRgb(50, 50, 50)),
"Header row should have dark background");Available Color Extension Methods
| Method | Purpose |
HasBackgroundColor() | Any cell has a background color |
HasBackgroundColor(Hex1bColor) | Any cell has specific background |
HasForegroundColor() | Any cell has a foreground color |
HasForegroundColor(Hex1bColor) | Any cell has specific foreground |
GetBackgroundColor(x, y) | Get background at position |
GetForegroundColor(x, y) | Get foreground at position |
HasUniformBackgroundColor(y, color) | All cells in row have same background |
VisualizeBackgroundColors() | Debug helper with visual representation |
Anti-Patterns to Avoid
📘 See the
test-fixerskill for detailed diagnosis and fixes when tests become flaky.
❌ Insufficient WaitUntil Conditions (Partial Render)
// BROKEN: Waits for partial content, but rest of screen may not be rendered
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Header"), TimeSpan.FromSeconds(2)) // ❌ Only checks header
.Capture("screen")
.Build()
.ApplyAsync(terminal, ct);
// Assertion on footer may fail - it wasn't part of the WaitUntil!
Assert.IsTrue(snapshot.ContainsText("Footer"));Problem: Rendering is inherently async. Finding "Header" doesn't guarantee "Footer" has rendered yet. This is especially problematic when testing other terminal frameworks (like Spectre Console) which may drop input if they're not ready to receive it.
Fix: Over-specify the WaitUntil condition to ensure everything you need is present:
// ✅ Wait for ALL content you'll assert on
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Header") && s.ContainsText("Footer"),
TimeSpan.FromSeconds(2), "full screen rendered")
.Capture("screen")
.Build()
.ApplyAsync(terminal, ct);Guideline: If you're going to assert on specific screen content, include it in the WaitUntil condition. Don't assume the rest of the screen is ready just because one part appeared.
❌ Snapshot After Exit
// BROKEN: Snapshot taken AFTER Ctrl+C clears the buffer
var snapshot = await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Hello"), TimeSpan.FromSeconds(2))
.Capture("final")
.Ctrl().Key(Hex1bKey.C) // Buffer may be cleared before snapshot!
.Build()
.ApplyWithCaptureAsync(terminal, ct);
Assert.IsTrue(snapshot.ContainsText("Hello")); // ❌ May fail on Linux CIFix: The WaitUntil already verified the content. If you need to assert, the WaitUntil serves as the assertion.
❌ Missing WaitUntil After Action
// BROKEN: No wait for render after Down()
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Item 1"), TimeSpan.FromSeconds(2))
.Down()
.Capture("after-down") // ❌ Render may not be complete!
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyAsync(terminal, ct);Fix: Always add WaitUntil after any action that changes state:
.Down()
.WaitUntil(s => s.ContainsText("> Item 2"), TimeSpan.FromSeconds(2), "moved down")
.Capture("after-down")❌ Task.Delay for Async Events
// BROKEN: Fixed delay may not be long enough on slow CI
await terminal.SendKeyAsync(Hex1bKey.Enter);
await Task.Delay(100); // ❌ Arbitrary delay
Assert.IsTrue(eventFired);Fix: Use TaskCompletionSource to signal completion:
var eventSignal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
// In event handler:
eventSignal.TrySetResult();
// In test:
await eventSignal.Task.WaitAsync(TimeSpan.FromSeconds(2), ct);❌ Over-Abstracted Test Helpers
// AVOID: Too many layers of abstraction
var result = await TestHelpers.CreateTerminalAndRunScenario(
widgets: WidgetFactory.CreateStandardList(),
actions: ActionBuilder.NavigateAndSelect(3),
assertions: AssertionBuilder.SelectedItem("Item 3")
);Prefer: Simple, linear, self-contained tests. Repetition is acceptable and helps AI agents understand patterns.
Presentation Escape Timeout Tests
Use queued in-memory presentation reads with Hex1bAppWorkloadAdapter. Observe
the escape timer's finite ITimer.Change before firing its callback; observing
CreateTimer alone is insufficient because it is initially disabled. Keep other
timers (such as synchronized output) independent in the controlled provider.
For example, Hex1bTerminalTests.PresentationInput_PasteEscapeAcrossTimeout_PreservesLiteralContent
queues "\x1b[200~a\x1b", waits for arming, fires expiry, then queues
"\rb\x1b[201~z". Read through the post-paste z sentinel before asserting
exact completed paste text and no surplus events. This avoids sleeps and makes
read-boundary/timeout bugs reproducible. At app level, bind on the focused widget
and prove the same Escape binding fires for an ordinary Escape after the paste.
Network Test Fixtures
Let the server reserve its listening port atomically. Random port selection and
probing a free port before closing the probe both allow collisions under parallel
execution. For Kestrel fixtures, configure
options.Listen(IPAddress.Loopback, 0), await app.StartAsync(), then derive client
URIs from TestSeq.Single(app.Urls). For example:
var wsUri = new UriBuilder(TestSeq.Single(app.Urls))
{
Scheme = "ws",
Path = "/ws/attach"
}.Uri;Keep the application owned by the fixture before awaiting startup so cleanup can
dispose it even if startup fails. Cover independently reachable concurrent servers
and listener release; see RemoteTerminalWorkloadAdapterTests. Do not mask port
collisions with sleeps, retries, or disabled parallelism.
Scheduler Progress Under Load
For starvation regressions, keep the producer active while asserting input, timer,
or shutdown progress. A finite output burst followed by app.Invalidate() can
hide a lost wakeup. Use TestWidget.OnRender to place events at a known frame:
var observer = new TestWidget().OnRender(args =>
{
app.Invalidate(); // Renew on every frame, including while input is pending.
if (args.RenderCount == 3)
workload.SendKey(Hex1bKey.A);
});This fragment assumes captured app and workload references. Pair it with
changing visible content so frames actually render, a bounded completion signal,
and cancellation in finally. See Hex1bAppSchedulingTests for full examples.
Check input ordering with coalescing both enabled and disabled. For exact cadence,
use the app's internal FrameTimeProvider with a fake clock, wait for timer
registration before advancing it, and assert both the requested delay and elapsed
virtual time. A fixed wall-clock tolerance around Task.Delay is not portable
across CI runners. Keep real-time full-stack tests alongside deterministic pacing
coverage rather than widening timing tolerances.
For nested output races, gate later child redraws: their extra notifications can
mask a lost first-frame notification. These controlled cases supplement, rather
than prove, responsiveness under arbitrary real-world load.
Process Output Completion
Process exit and terminal output consumption are separate events. For a controlled
regression, start a StandardProcessWorkloadAdapter and await its exit before
constructing a terminal with an already-completed run callback. Gate the first
presentation write with a TaskCompletionSource: RunAsync and lifecycle
completion must remain pending until the gate is released and both stdout and
stderr reach the snapshot. See StandardProcessOutputTests for raw and filtered
output, cancellation, and pump-failure cases. Keep ordinary WithProcess tests
alongside this ordering test; do not keep a one-shot child alive or wait for visible
output before awaiting RunAsync in a drain regression, since that hides the race.
For echo/transport tests, use an already-available executable (cmd /d /c echo on
Windows, /bin/echo on Unix). Runtime-compiling a temporary C# program with
dotnet run puts SDK startup and compilation inside the output deadline.
Unix PTY Read Reliability
Use /bin/sh with stty -echo, a newline-free readiness marker, and input-gated
output to test the real PTY without interactive shell startup or echo ambiguity.
Accumulate reads until the exact expected marker or EOF: one child write need not
equal one parent read. Drain large output concurrently with the child writing;
also exercise WaitForExitAsync concurrently with draining to cover reaping.
See UnixPtyReadTests for exact 64 KB payloads and repeated idle handshakes.
Include teardown without reading any child output, not only disposal after a
readiness handshake. On macOS, a child can remain in exit while its PTY output
is undrained; disposal must release the master before blocking on reaping.
Cancellation is not a native-hang watchdog. Bound reads and teardown independently
with WaitAsync; run potentially synchronous native writes/disposal on a worker:
await Task.Run(async () => await process.DisposeAsync())
.WaitAsync(TimeSpan.FromSeconds(10));Cancel outstanding reads and dispose the child in finally. For disposal tests,
leave the pending read uncanceled so disposal itself must unblock it. An explicit
idle interval spanning poll timeouts may assert that a read remains pending; do
not use a sleep to guess when output arrived. High-descriptor tests must measure
the actual PTY descriptor and release every dummy handle even on failure. Only
the native descriptor-limit error is grounds for an explicit inconclusive result;
other open/start/read failures must fail the test.
HMP1 Shutdown Compatibility
Test each endpoint against a scripted peer with pinned numeric frame types, little-endian headers, and literal JSON from the pre-change protocol. Do not use the production codec on the simulated legacy side: otherwise both endpoints can change together and conceal a wire regression. Preserve the existing mandatory ActivityState handshake baseline; these fixtures model the build immediately before the shutdown change, not every historical HMP1 implementation.
See Hmp1ShutdownCompatibilityTests: the server fixture sends a literal
ClientHello and Input, gates a large Output write, then independently decodes
Output, Output, Exit and EOF without sending acknowledgements. The client fixture
feeds Hello, StateSync, ActivityState, split-UTF-8 Output and Exit or EOF, gates
presentation, and checks exact final bytes and the snapshot at lifecycle
completion. Include premature Exit and truncated final frames to establish that
missing content cannot be recovered. Confirm the relevant tests fail when server
ordering or client draining is temporarily removed, then restore both safeguards.
Widget Test Dimensions
When writing tests for widgets, consider all the dimensions that affect behavior. Each widget should have tests covering these scenarios:
1. Terminal Size Variations
Widgets must work across different terminal sizes. Use MSTest DataRow for parameterized cases:
[TestMethod]
[DataRow(40, 10)] // Minimum realistic size
[DataRow(80, 24)] // Standard terminal
[DataRow(120, 40)] // Large terminal
[DataRow(200, 60)] // Very large terminal
public async Task ListWidget_VariousTerminalSizes_RendersCorrectly(int width, int height)
{
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => new ListWidget(["Item 1", "Item 2", "Item 3"]))
.WithHeadless()
.WithDimensions(width, height)
.Build();
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Item 1"), TimeSpan.FromSeconds(2), "list rendered")
.Capture($"list-{width}x{height}")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyAsync(terminal, TestContext.Current.CancellationToken);
}Key questions to answer:
- What is the realistic minimum terminal size for this widget?
- Does the widget truncate, scroll, or wrap when space is limited?
- Does the widget expand appropriately in large terminals?
- Are there edge cases at specific sizes?
2. Container Widget Context
Widgets behave differently depending on their parent container. Test inside various layouts:
[TestMethod]
public async Task ProgressWidget_InsideBorder_RendersWithCorrectWidth()
{
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => new BorderWidget(
new ProgressWidget { Value = 50, Maximum = 100 },
title: "Loading"
))
.WithHeadless()
.WithDimensions(60, 10)
.Build();
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Loading"), TimeSpan.FromSeconds(2), "border rendered")
.Capture("progress-in-border")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyAsync(terminal, TestContext.Current.CancellationToken);
}
[TestMethod]
public async Task Button_InsideHStack_SharesSpaceCorrectly()
{
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => new HStackWidget([
new ButtonWidget("Cancel"),
new ButtonWidget("OK")
]))
.WithHeadless()
.WithDimensions(40, 5)
.Build();
await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Cancel") && s.ContainsText("OK"), TimeSpan.FromSeconds(2))
.Capture("buttons-in-hstack")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyAsync(terminal, TestContext.Current.CancellationToken);
}Common container scenarios to test:
- Inside
VStackWidget(vertical stacking) - Inside
HStackWidget(horizontal stacking) - Inside
BorderWidget(reduced available space) - Inside
ScrollPanelWidget(scrollable content) - Inside
SplitterWidget(resizable panes) - Nested containers (e.g., Border inside VStack inside Splitter)
3. Theming Behavior
Verify that widgets respect theme colors and can be customized:
[TestMethod]
public async Task Button_WithCustomTheme_UsesThemeColors()
{
var customTheme = new Hex1bTheme("TestTheme")
.Set(ButtonTheme.BackgroundColor, Hex1bColor.FromRgb(255, 0, 0))
.Set(ButtonTheme.ForegroundColor, Hex1bColor.FromRgb(255, 255, 255));
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) =>
{
options.Theme = customTheme;
return ctx => new ButtonWidget("Test Button");
})
.WithHeadless()
.WithDimensions(40, 5)
.Build();
var snapshot = await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Test Button"), TimeSpan.FromSeconds(2))
.Capture("themed-button")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
Assert.IsTrue(snapshot.HasBackgroundColor(Hex1bColor.FromRgb(255, 0, 0)),
"Button should have red background from theme");
Assert.IsTrue(snapshot.HasForegroundColor(Hex1bColor.FromRgb(255, 255, 255)),
"Button should have white text from theme");
}
[TestMethod]
public async Task Button_FocusedState_UsesFocusedThemeColors()
{
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithHex1bApp((app, options) => ctx => new VStackWidget([
new TextBlockWidget("Header"),
new ButtonWidget("Focusable Button")
]))
.WithHeadless()
.WithDimensions(40, 5)
.Build();
var snapshot = await new Hex1bTerminalInputSequenceBuilder()
.WaitUntil(s => s.ContainsText("Focusable Button"), TimeSpan.FromSeconds(2))
.Tab() // Focus the button
.WaitUntil(s => s.ContainsText(">"), TimeSpan.FromSeconds(2), "button focused")
.Capture("focused-button-theme")
.Ctrl().Key(Hex1bKey.C)
.Build()
.ApplyWithCaptureAsync(terminal, TestContext.Current.CancellationToken);
// Verify focused state uses different colors than unfocused
Assert.IsTrue(snapshot.HasBackgroundColor(), "Focused button should have background color");
}Theming scenarios to test:
- Default theme renders correctly
- Custom theme colors are applied
- Focused vs unfocused states use appropriate theme values
- Disabled state styling (if applicable)
- Theme inheritance from parent widgets
4. Widget Test Matrix
For comprehensive widget coverage, consider this matrix:
| Dimension | Variations to Test |
| Terminal Size | Minimum (40×10), Standard (80×24), Large (120×40), Very Large (200×60) |
| Container | Root, VStack, HStack, Border, Scroll, Splitter, Nested |
| Theme | Default, Custom colors, Focused state, Disabled state |
| Content | Empty, Minimal, Typical, Maximum/overflow |
| State | Initial, After interaction, Edge cases |
Not every widget needs every combination, but consider which dimensions are relevant for the widget's behavior.
Low-Level API Testing (Isolation)
Output-Pump Allocation Regressions
Measure GC.GetAllocatedBytesForCurrentThread() around a synchronous application
region, not across awaits or for the whole process. A workload filter returning
ValueTask.CompletedTask can start the measurement after parsing; the terminal's
PresentationInvalidated callback can finish it. Assert both callbacks used the
same thread. Use a large batch of allocation-free tokens (such as SGR resets)
and a byte budget that excludes per-token bookkeeping but allows fixed overhead.
Keep parsing and HWT frame generation outside the measured region, then separately
verify frame delivery and batch accounting. See Hwt1ImpactCollectionTests for
raw, pre-tokenized, and HMP StateSync coverage. Confirm the guard fails when the
optimization is disabled; behavior-only assertions do not prove allocation removal.
Keyboard Wire Conformance
Use literal expected bytes independent of the production key/text mapper. For
example, Alt+Shift+E is 1B45, while Ctrl+Alt+E is 1B05 in Hex1b's legacy
automation profile. Exercise the public automator and sequence builder against a
recording workload, asserting immediately after awaited sends rather than sleeping.
See TerminalKeyboardMatrixTests for the key/modifier/cursor-mode/keypad-mode
matrix and completeness checks that fail when an enum grows. Include modifier
reset, overlap, ordering, and replay after mode changes; constructing a sequence
must not freeze its wire encoding. Keep physical layout, AltGr/IME, and negotiated
keyboard protocols distinct from this logical-key encoding contract.
Workload Binary Compatibility
Compile a separate fixture against a pinned published Hex1b package, not the
current project, and load that unchanged adapter assembly against the current
library. Assert that its implemented interface resolves to the current assembly
and that newly added default members preserve raw input delivery. Recompiling a
test adapter against the new interface proves source compatibility, not binary
compatibility. tests/Fixtures/LegacyWorkloadAdapter also provides a build-time
stdin probe: it reports exact bytes read by a child process, without runtime
compilation or depending on the child's text encoding.
Native Windows Console Probes
Run native console tests in a child process under WindowsProxyPtyHandle, not
against the test runner's own console. WindowsConsoleProbeTests launches the
already-built test executable with an exact --filter and a child-only
environment marker; its guarded child test constructs the real console driver.
The parent acts as the terminal, waits for an explicit probe-start marker before
replying, and checks a result written through the driver. This exercises ConPTY and
ReadConsoleInputW without runtime compilation or shared-console mutation.
Do not synchronize on the outgoing KGP query: some ConPTY hosts consume APC
queries instead of forwarding them, even though input can still be tested.
Keep the existing test-host packaging unchanged rather than changing the host
for unrelated PTY tests to satisfy this fixture. Use bounded
cancellation and dispose the PTY to terminate children on assertion failures.
For APIs that are dependencies of Hex1bApp (like Surface), test in isolation:
[TestMethod]
public void Surface_WriteText_SetsCorrectCells()
{
// Arrange
var surface = new Surface(80, 24);
// Act
surface.WriteText(0, 0, "Hello");
// Assert
Assert.AreEqual('H', surface[0, 0].Character[0]);
Assert.AreEqual('e', surface[1, 0].Character[0]);
Assert.AreEqual('l', surface[2, 0].Character[0]);
Assert.AreEqual('l', surface[3, 0].Character[0]);
Assert.AreEqual('o', surface[4, 0].Character[0]);
}
[TestMethod]
public void SurfaceCell_WithColor_PreservesColor()
{
// Arrange
var cell = new SurfaceCell('X', Hex1bColor.Red, Hex1bColor.Blue);
// Assert
Assert.AreEqual('X', cell.Character[0]);
Assert.AreEqual(Hex1bColor.Red, cell.Foreground);
Assert.AreEqual(Hex1bColor.Blue, cell.Background);
}Test Naming Convention
Follow MethodName_Scenario_ExpectedBehavior:
[TestMethod]
public async Task ListWidget_DownArrow_SelectsNextItem() { }
[TestMethod]
public async Task TextBox_TypeText_DisplaysInput() { }
[TestMethod]
public async Task Button_EnterKey_TriggersClickHandler() { }
[TestMethod]
public void Surface_Fill_SetsAllCellsInRegion() { }Updating This Skill
When you discover a new testing pattern while writing tests:
- Add the pattern to this skill as part of the same PR
- Include a concrete example with comments
- Explain when to use it (what problem does it solve?)
- If it's an anti-pattern, add it to the anti-patterns section with the fix
This builds the body of knowledge available to AI agents working on the codebase.
Examples of Patterns to Document
- New assertion helpers or extension methods
- Patterns for testing specific widget types
- Workarounds for platform-specific behavior
- Performance testing patterns
- Patterns for testing async behavior
Checklist for New Tests
- Uses
Hex1bTerminal.CreateBuilder()with.WithHeadless() - Uses
.WithHex1bApp()for TUI functionality (unless testing low-level APIs) - Has
WaitUntilafter every action that changes state - Has
WaitUntilimmediately before.Capture() - Uses descriptive wait messages (third parameter to
WaitUntil) - Exits cleanly with
Ctrl().Key(Hex1bKey.C) - Follows
MethodName_Scenario_ExpectedBehaviornaming - Is simple and linear (no unnecessary abstractions)
- Asserts on colors when testing themed/styled widgets
- Uses
CellPatternSearcherfor precise cell assertions when needed
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 4
- TriliumNext/Trilium★ 39ksame name
- Asvarox/allkaraoke★ 269same name
- jaktestowac/awesome-copilot-for-testers★ 118same name
- jjmrocha/coding-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- aspireSkill
- playwright-cliSkill
- api-reviewerSkill
- aspireSkill
- doc-testerSkill
- doc-writerSkill
- playwright-cliSkill
- surface-benchmarkerSkill
- test-fixerSkill
- widget-creatorSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

