Agent skill

Writing Unit Tests

by mitchdenny in mitchdenny/hex1b

Guidelines for writing unit tests in the Hex1b TUI library. An agent skill from mitchdenny/hex1b.

MITAuto-check passedTesting & QA

Install Writing Unit Tests

skills CLI
$ npx skills add mitchdenny/hex1b --skill writing-unit-tests -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install mitchdenny/hex1b writing-unit-tests --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/mitchdenny/hex1b.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.github/skills/writing-unit-tests .claude/skills/writing-unit-tests && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
writing-unit-tests
GitHub stars
178
Token cost
~7k tokens
SKILL.md length
2,012 words
Files
1
Skills in repo
7
Repo updated
First seen
Licence
MIT

At a glance

Guidelines for writing unit tests in the Hex1b TUI library. An agent skill from mitchdenny/hex1b.

  • Works in 4 steps: Terminal Size Variations → Container Widget Context → Theming Behavior → …
  • Creating new tests for widgets
  • SKILL.md covers Core Philosophy, When to Use Full Stack vs…, Standard Test Structure and Input Sequencing Patterns, plus 8 more sections
  • Calls dotnet

What it does

Writing Unit Tests is an agent skill from mitchdenny/hex1b. Guidelines for writing unit tests in the Hex1b TUI library. Use when creating new tests for widgets, nodes, or terminal functionality.

Its SKILL.md is about 7k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Testing & QA, covering Unit testing. It works with .NET. The repository describes itself as: The .NET Terminal Application Stack. The licence is MIT.

When your agent uses it

  • Creating new tests for widgets
  • Terminal functionality

Example prompts

  • “Use the writing-unit-tests skill to guideline for writing unit tests in the Hex1b TUI library. An agent skill from mitchdenny/hex1b”
  • “/writing-unit-tests”

Workflow steps

4 steps, taken from the step headings in SKILL.md.

  1. Terminal Size Variations
  2. Container Widget Context
  3. Theming Behavior
  4. Widget Test Matrix

What it can do on your machine

Read from SKILL.md and the folder at commit e2335bd. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • dotnet

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Writing Unit Tests loads about 7k tokens when it runs. Until then it costs about 38 tokens; SKILL.md has 2,012 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~38
When it runs · the whole SKILL.md, loaded when a task matches
~7k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from mitchdenny/hex1b at commit e2335bd, republished under its MIT licence (© mitchdenny). 2,012 words, ~7,015 tokens.

Download SKILL.mdSave it as .claude/skills/writing-unit-tests/SKILL.md (or your agent's skills folder).
name
writing-unit-tests
description
Guidelines for writing unit tests in the Hex1b TUI library. Use when creating new tests for widgets, nodes, or terminal functionality.

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

  1. Prefer full terminal stack testing - Use Hex1bTerminal.CreateBuilder() to create complete terminal environments
  2. Use .WithHex1bApp() for TUI functionality tests - This wires up the full app lifecycle
  3. Keep tests simple and linear - Avoid excessive abstractions; repeating patterns are beneficial for AI agents
  4. Assert on visual behavior - Use CellPatternSearcher and color assertions for render verification
  5. Update this skill when discovering new patterns - Build the body of knowledge as part of PRs

When to Use Full Stack vs Isolation

Test TypeApproach
Widget behavior, layout, renderingFull stack with Hex1bTerminal.CreateBuilder()
Input handling, focus navigationFull stack with WithHex1bApp()
Low-level APIs (Surface, SurfaceCell)Test in isolation (dependencies of Hex1bApp)
Color/theme verificationFull 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:

csharp
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
  1. await using var terminal - Ensures proper disposal
  2. .WithHeadless() - No actual terminal output (CI-safe)
  3. .WithDimensions(80, 24) - Explicit terminal size
  4. WaitUntil before assertions - Prevents timing issues
  5. .Capture("name") - Saves SVG/HTML for debugging
  6. Ctrl().Key(Hex1bKey.C) - Clean exit

Input Sequencing Patterns

Basic Navigation
csharp
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
csharp
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
csharp
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:

csharp
// 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:

csharp
// 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
MethodPurpose
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-fixer skill for detailed diagnosis and fixes when tests become flaky.

❌ Insufficient WaitUntil Conditions (Partial Render)
csharp
// 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:

csharp
// ✅ 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
csharp
// 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 CI

Fix: The WaitUntil already verified the content. If you need to assert, the WaitUntil serves as the assertion.

❌ Missing WaitUntil After Action
csharp
// 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:

csharp
.Down()
.WaitUntil(s => s.ContainsText("> Item 2"), TimeSpan.FromSeconds(2), "moved down")
.Capture("after-down")
❌ Task.Delay for Async Events
csharp
// 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:

csharp
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
csharp
// 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:

csharp
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:

csharp
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.

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:

csharp
[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?
Show full SKILL.md (777 more words)Show less
2. Container Widget Context

Widgets behave differently depending on their parent container. Test inside various layouts:

csharp
[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:

csharp
[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:

DimensionVariations to Test
Terminal SizeMinimum (40×10), Standard (80×24), Large (120×40), Very Large (200×60)
ContainerRoot, VStack, HStack, Border, Scroll, Splitter, Nested
ThemeDefault, Custom colors, Focused state, Disabled state
ContentEmpty, Minimal, Typical, Maximum/overflow
StateInitial, 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:

csharp
[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:

csharp
[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:

  1. Add the pattern to this skill as part of the same PR
  2. Include a concrete example with comments
  3. Explain when to use it (what problem does it solve?)
  4. 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 WaitUntil after every action that changes state
  • Has WaitUntil immediately before .Capture()
  • Uses descriptive wait messages (third parameter to WaitUntil)
  • Exits cleanly with Ctrl().Key(Hex1bKey.C)
  • Follows MethodName_Scenario_ExpectedBehavior naming
  • Is simple and linear (no unnecessary abstractions)
  • Asserts on colors when testing themed/styled widgets
  • Uses CellPatternSearcher for precise cell assertions when needed

© mitchdenny, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .github/skills/writing-unit-tests of mitchdenny/hex1b.

Open the folder on GitHubat commit e2335bd

Compare with similar skills

Writing Unit Tests next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Writing Unit Tests compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Writing Unit Tests this skillmitchdenny/hex1b178—~7kAutomated safety check: PassMIT
New Event Sourceaws/aws-lambda-dotnet1.7k—~3kAutomated safety check: PassApache-2.0
ScottPlot Test RunnerScottPlot/ScottPlot6.8k—~308Automated safety check: PassMIT
Aspire Integration TestingDevBetterCom/DevBetterWeb1572 repos~2.3kAutomated safety check: PassNone
Vstest Build Testmicrosoft/vstest969—~1.9kAutomated safety check: PassMIT
Coverage Analysisrunceel/ReactiveProperty944—~5.9kAutomated safety check: WarnMIT

Similar skills

  • New Event Source

    aws/aws-lambda-dotnet

    Official

    Add a new AWS event source attribute (e.g., Kinesis, Kafka, MQ) to the Lambda .NET Annotations framework, including the attribute class, source generator integration, CloudFormation writer, unit…

    1.7k GitHub stars~3k tokensUpdated today
    Testing & QAAuto-check passed
  • ScottPlot Test Runner

    ScottPlot/ScottPlot

    Run or add ScottPlot 5 tests. Use for unit-test and cookbook-test work; unless explicitly asked otherwise, restrict manual test execution to the Unit Tests…

    6.8k GitHub stars~308 tokensUpdated 1 mo ago
    Testing & QAAuto-check passed
  • Aspire Integration Testing

    DevBetterCom/DevBetterWeb

    Write integration tests using .NET Aspire's testing facilities with xUnit.

    157 GitHub starsUsed in 2 repos~2.3k tokens
    Testing & QAAuto-check passed
  • Vstest Build Test

    microsoft/vstest

    Official

    Build, test, and validate changes in the vstest repository. An agent skill from microsoft/vstest.

    969 GitHub stars~1.9k tokensUpdated yesterday
    Testing & QAAuto-check passed
  • Coverage Analysis

    runceel/ReactiveProperty

    Automated, project-wide code coverage and CRAP (Change Risk Anti-Patterns) score analysis for .NET projects with existing unit tests.

    944 GitHub stars~5.9k tokensUpdated 1 mo ago
    Testing & QAAuto-check: warnings
  • Unit Testing

    OpenCoreMMO/OpenCoreMMO

    Write, fix, or review NeoServer unit tests using xUnit and FluentAssertions.

    481 GitHub stars~1.6k tokensUpdated 16 days ago
    Testing & QAAuto-check passed

More from mitchdenny/hex1b

  • API Reviewer

    mitchdenny/hex1b

    Guidelines for reviewing API design in the Hex1b codebase. An agent skill from mitchdenny/hex1b.

    178 GitHub stars~4k tokensUpdated today
    Auto-check passed
  • Surface Benchmarker

    mitchdenny/hex1b

    Guidelines for running and interpreting Surface API performance benchmarks.

    178 GitHub stars~3.1k tokensUpdated today
    Auto-check passed
  • Doc Tester

    mitchdenny/hex1b

    Agent for validating Hex1b documentation against actual library behavior.

    178 GitHub stars~9.5k tokensUpdated today
    Auto-check passed
  • Doc Writer

    mitchdenny/hex1b

    Guidelines for producing accurate and maintainable documentation for the Hex1b TUI library.

    178 GitHub stars~8.7k tokensUpdated today
    Auto-check passed
  • Test Fixer

    mitchdenny/hex1b

    Agent for diagnosing and fixing flaky terminal UI tests in the Hex1b test suite.

    178 GitHub stars~6.5k tokensUpdated today
    Auto-check passed
  • Widget Creator

    mitchdenny/hex1b

    Step-by-step guide for creating new widgets in the Hex1b TUI library.

    178 GitHub stars~7.6k tokensUpdated today
    Auto-check passed

Works with

Categories

Questions about Writing Unit Tests

What does Writing Unit Tests do?

Guidelines for writing unit tests in the Hex1b TUI library. An agent skill from mitchdenny/hex1b. Writing Unit Tests is an agent skill from mitchdenny/hex1b. Guidelines for writing unit tests in the Hex1b TUI library.

When should I use Writing Unit Tests?

Writing Unit Tests fits situations like: creating new tests for widgets; terminal functionality.

How do I install Writing Unit Tests in Claude Code?

Run `npx skills add mitchdenny/hex1b --skill writing-unit-tests -a claude-code`. Or copy the skill folder (.github/skills/writing-unit-tests in mitchdenny/hex1b) into .claude/skills/writing-unit-tests in your project. Claude Code loads it when a task matches its description.

How do I install Writing Unit Tests in Codex?

Run `npx skills add mitchdenny/hex1b --skill writing-unit-tests -a codex`. Or copy the skill folder (.github/skills/writing-unit-tests in mitchdenny/hex1b) into .agents/skills/writing-unit-tests in your project. Codex loads it when a task matches its description.

Can I use Writing Unit Tests in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add mitchdenny/hex1b --skill writing-unit-tests -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/writing-unit-tests, .gemini/skills/writing-unit-tests, .github/skills/writing-unit-tests and .opencode/skills/writing-unit-tests in your project.

What does Writing Unit Tests need to run?

Going by SKILL.md and its folder, Writing Unit Tests needs the command-line tools its instructions call (dotnet).

Does Writing Unit Tests access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Writing Unit Tests safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Writing Unit Tests use?

Writing Unit Tests is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Writing Unit Tests use?

About 7k tokens (SKILL.md is roughly 28k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Writing Unit Tests?

Skills that share tags, products or a category with Writing Unit Tests: New Event Source (aws/aws-lambda-dotnet, 1.7k stars), ScottPlot Test Runner (ScottPlot/ScottPlot, 6.8k stars), Aspire Integration Testing (DevBetterCom/DevBetterWeb, 157 stars) and Vstest Build Test (microsoft/vstest, 969 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Writing Unit Tests?

mitchdenny (a GitHub user) maintains it in mitchdenny/hex1b, which has 178 GitHub stars. The repository holds 7 skills in this directory. The repository was last updated on October 9, 2026.

Source: mitchdenny/hex1b on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.