drawnui/drawnui.net/drawnui-net-harness

Claude Code and agent skill by drawnui/drawnui.net

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use for headless, pure-.NET (no device/GPU) testing and runtime debugging of DrawnUi controls — render frames, simulate gestures, settle background measurement, detect blank/empty renders, reproduce SkiaScroll/SkiaLayout virtualization + windowed-list bugs. Trigger on "test in .net harness", "headless", "repro without device", DrawnUi.Testing, HeadlessCanvasHost, GestureRobot, VirtualizationProbe.

Install

npx vskill@latest install drawnui/drawnui.net/drawnui-net-harness

SKILL.md

DrawnUi .NET Headless Harness

Pure managed DrawnUi runs WITHOUT a window, device, or GPU. Render to an offscreen SkiaSharp surface on a deterministic synthetic clock, drive real gestures, inspect structure + pixels. Ideal for unit tests AND for reproducing/fixing layout/scroll/virtualization bugs the way the device does — fast, deterministic, scriptable. Prefer this over OpenTk for measurement/scroll/windowing bugs.

Where it lives (repo DrawnUi)

  • Infra: src/Net/DrawnUi/Testing/ — HeadlessCanvasHost.cs, GestureRobot.cs, VirtualizationProbe.cs, VirtualizationScene.cs.
  • Runnable harness sample: src/Net/Samples/VirtualizationHarnessDemo/ — Program.cs (top-level entry), ChatLikeScene.cs (inverted+windowed chat repro), BlankJumpRepro.cs.
  • Also on this host: src/Net/Samples/GestureHarnessDemo/ and the xUnit project src/Net/Tests/DrawnUi.Net.Tests/ (assert-style regression tests). src/Net/Samples/SkiaEditorHarness/ carries its own private copy of the host.
  • Namespaces: DrawnUi.Testing, DrawnUi.Draw. Project targets net9.0, refs DrawnUi.Net by project → editing engine src/Shared/** recompiles into the harness directly.

Core API

csharp
var host = new HeadlessCanvasHost(width:430, height:720, scale:1f, background:Colors.Black);
host.Canvas.Content = root; // any SkiaControl tree (SkiaScroll, SkiaLayout, ...)
host.RenderFrame(16); // advance synthetic clock 16ms + render one frame
host.AdvanceFrames(12, 16); // N frames
host.SavePng(path); // visual diff
double fill = host.NonBackgroundFraction(Colors.Black); // 0..1 ; ~0 on a populated scene == BLANK
  • Clock is synthetic + monotonic: gestures (flushed in ExecuteBeforeDraw) and fling/animation (ticked off FrameTimeNanos) advance only when you call RenderFrame. Nothing happens "between" frames.
  • Super.Init() runs once inside the host ctor.
Gestures — really wired
csharp
var robot = new GestureRobot(host);
robot.Pan(fromX, fromY, toX, toY, durationMs:90, steps:8); // finger up (520->180) scrolls content DOWN
robot.Tap(x, y); robot.Fling(...); // see GestureRobot.cs for full set
Virtualization probe (any templated SkiaScroll+SkiaLayout)
csharp
var probe = new VirtualizationProbe(host, scroll, list);
probe.Frontier; // list.LastMeasuredIndex (measurement frontier)
probe.FirstVisible / probe.LastVisible / probe.OffsetY / probe.ContentHeight / probe.ItemsCount;
probe.SettleBackground(maxFrames:120); // render until the frontier stops advancing
probe.Drive(robot, stop:()=>..., down:true); // gesture-drive until a condition

Building a faithful scene (CRITICAL)

A harness only proves something if it mimics the target's DEFINING conditions. For ChatPage-class bugs that means: inverted (SkiaScroll.Rotation=180, ReverseGestures=true, TrackIndexPosition=Start), windowed ItemsSource (ObservableRangeCollection, slice newest-first Items[i]==All[End-1-i], cap resident count), bidirectional LoadMore (LoadMoreCommand=older/append-tail, LoadMoreTopCommand=newer/head-insert), variable heights, MeasureVisible strategy, jump buttons (WindowedSource.ScrollToIndex(global, align, animate): ReplaceRange rebase + scroll.ScrollToIndex(local, animate, align, clamp:true); every ScrollToIndex is an ordered scroll, held until it arrives). ChatLikeScene.cs is the reference; copy it.

Windowing is now a LIBRARY primitive — no subclass needed: DrawnUi.Draw.WindowedSource<T> (sliding window over a big backing list) + SkiaScrollWindowHost (built-in IWindowHost adapter) over a plain SkiaLayout. Base SkiaLayout provides SuppressLoadMore, ordered-scroll LoadMore gating (inside ShouldTriggerLoadMore), and the MeasurementApplied event (trim-after-measure hook). ChatLikeScene drives these directly. Do NOT validate on a simplified static scene (uniform heights, non-inverted, non-windowed) — it proves nothing.

Detecting a blank/empty render headlessly

NonBackgroundFraction(bg) ~ 0 on a scene that should be full = blank. Pair with structure dump from a SkiaLayout subclass (StackStructure is protected): print each cell ControlIndex / Destination.Top / Bottom / Measured.Height. A non-monotonic Top sequence or h0 cells = corrupt structure (overlap). Content height is computed O(1) as last.Destination.Bottom - first.Destination.Top (UpdateProgressiveContentSize), so a wrong LAST-cell position collapses content → offset exceeds it → blank.

Run it

dotnet run --project src/Net/Samples/VirtualizationHarnessDemo/VirtualizationHarnessDemo.csproj -c Debug

For engine Debug.WriteLine on stdout, add Trace.Listeners.Add(new ConsoleTraceListener()) in the entry (remove after). Gate temp engine probes behind a public static bool DebugX flag and REMOVE when done — keep instrumentation out of committed engine code.

Gotchas learned

  • Pixel-asserting an ImageDoubleBuffered control: the offscreen bake runs on a REAL worker thread while the harness clock is synthetic — N AdvanceFrames alone can snapshot before the bake lands → canvas all-background, flaky. Settle first: for(...40){ if (host.NonBackgroundFraction(bg) > 0.001) break; Thread.Sleep(20); host.AdvanceFrames(3); }. Property/structure asserts don't need this. (Verified 2026-07, SkiaSlider style probes.)
  • Background-measured cells must set BOTH cell.Area and cell.Destination. ComputeBottomOfRow (which positions the NEXT batch) reads cell.Area.Top — leaving Area default (Top=0) makes every cell report bottom=height, so the next batch stacks at the top → overlap → collapsed content → blank. (Root cause of the 2026 item-measurement-memo blank-jump bug.)
  • _measuredItems is keyed by LOCAL index → a full-collection ReplaceRange (window jump) clears it (ResetMeasurementForReplace). An item-keyed measurement memo (MeasurementCacheCapacity, default 1000) survives swaps; it must seed Area too.
  • MEMO/fast paths complete background measurement in fewer frames → can expose latent multi-batch ordering races that the slow path hid. Always A/B the harness with the optimization ON vs OFF (e.g. MeasurementCacheCapacity: 0) to attribute a regression.
  • Transient mid-settle collapse (incomplete frontier) is benign and self-heals; a STICKY collapse is the bug. Settle fully before asserting.
  • VirtualizationProbe.SettleBackground returns on frontier-STABLE, which a stalled-but-incomplete frontier also satisfies → false-blank snapshots / flaky runs. For jump assertions settle until Frontier >= ItemsCount-1 && LastVisible >= 0 (background measurement re-triggers each rendered frame). Killed the blank-jump flakiness.
  • Background measurement runs on real threadpool threads (Task.Run) with Thread.Sleep pacing in settle → naive single-run assertions are flaky. Run the scenario N times and assert 0 failures.
  • xUnit runs test classes in parallel in ONE process: a test that writes process-wide engine state (Super.Screen.Density, Super.Multithreaded, Super.EnableRendering / IsRtl / FontSubPixelRendering, clipboard, font registration) goes in a [CollectionDefinition(..., DisableParallelization = true)] collection. Setting Super.Screen.Density resets RenderingScale on every live canvas, so canvases of other test classes running at the same time draw a frame at that density (0 gives infinite sizes). Sign of a missing collection: failures only in full parallel runs, a different test each time, every test passes alone.
  • A static list in a test class (binds, events) also collects from controls of earlier cases still measuring in the background: filter by the current control.
  • Diagnose such failures with a per-frame trace inside the failing test (frame time, animator start / last frame time, MeasuredSize) rather than by reading code.
  • Reach for this harness first for measurement/scroll/windowing work.
  • AdvanceFrames draws EVERY frame, a real host draws only when a frame was asked for. A bug that waits for a frame nobody requests passes headless and fails in the app (2026-10-02: a ScrollToIndex into a rebased window stalled on WPF while every headless test passed). For anything that holds, retries or animates, also run it the way an app does: for (...; host.NeedsFrame; ...) host.RenderFrame(16); On WPF / OpenTK / Wasm, Update() raised while a frame is being drawn is dropped; ask with DrawnView.RequestNextFrame() instead.
  • Proving a fix in the real WPF app without the mouse: UI Automation. The canvas exposes its accessibility snapshot as UIA children (buttons Invoke, labels, rects), so a PowerShell UIA script can press drawn buttons by name (-ceq: PowerShell -eq is case-insensitive, "HOME" matched the navbar "Home") and read which cells are on screen. Launch your OWN instance; a Release window's GPU surface comes out blank with PrintWindow, a Debug one captures.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/drawnui/drawnui.net/drawnui-net-harness/badge)](https://verified-skill.com/skills/drawnui/drawnui.net/drawnui-net-harness)

Something wrong with this skill or its page? Tell us.