InkkOops

Authoring scripts

This page describes the script authoring contract in detail: how scripts are registered, how selectors work, when waits are required, and how to choose between pointer actions and semantic actions.

Script contract

A script definition implements IInkkOopsScriptDefinition. In this repository, built-in scripts usually implement IInkkOopsBuiltinScript, which is a host-facing convenience interface.

using InkkSlinger;

public sealed class SidebarRichTextBoxScript : IInkkOopsBuiltinScript
{
    public string Name => "sidebar-richtextbox";

    public InkkOopsScript CreateScript()
    {
        return new InkkOopsScriptBuilder(Name)
            .WaitForIdle(InkkOopsIdlePolicy.DiagnosticsStable)
            .Click("RichTextBox")
            .CaptureFrame("after-click")
            .DumpTelemetry("after-click")
            .Build();
    }
}

Name is the public entry point. CreateScript() must return a complete script object.

How scripts are discovered

The runtime depends on the configured script catalog, not on reflection directly. In this repository, the default host profile uses a reflection-backed catalog for convenience, so adding a built-in script class is usually enough to expose it. The important architectural point is that registration is a host concern.

Selectors

Selectors are the preferred way to target elements. They are more stable than coordinates because they survive layout shifts, window size changes, and template changes.

var byName = InkkOopsTargetSelector.Name("CatalogSidebarScrollViewer");
var byAutomationId = InkkOopsTargetSelector.AutomationId("PreviewHost");
var byAutomationName = InkkOopsTargetSelector.AutomationName("RichTextBox");

var scoped =
    InkkOopsTargetSelector.Within(
        InkkOopsTargetSelector.Name("CatalogSidebarScrollViewer"),
        InkkOopsTargetSelector.AutomationName("RichTextBox"));

var indexed =
    InkkOopsTargetSelector.DescendantOf(
        InkkOopsTargetSelector.Name("ToolbarHost"),
        InkkOopsTargetSelector.AutomationName("Delete"))
    .WithIndex(1);

The names above come from this repository's Controls Catalog host profile. They are examples, not hard requirements of the InkkOops core.

Resolution rules

  • Resolution prefers x:Name.
  • If no x:Name match exists, automation id is considered next.
  • If no automation id match exists, automation name is considered next.
  • If multiple matches are found and no index is specified, the command fails as Ambiguous.
  • If nothing matches, the command fails as Unresolved.

Wait semantics

Use waits that match the actual condition you need.

  • WaitForElement: target resolves.
  • WaitForVisible: target resolves and is visible.
  • WaitForEnabled: target resolves and is enabled.
  • WaitForInViewport: target resolves and its action point is in the viewport.
  • WaitForInteractive: target can be used for pointer interaction.
  • WaitForIdle: the host reaches the requested idle policy.
var richTextBoxButton = InkkOopsTargetSelector.Within(
    InkkOopsTargetSelector.Name("CatalogSidebarScrollViewer"),
    InkkOopsTargetSelector.AutomationName("RichTextBox"));

return new InkkOopsScriptBuilder("sidebar-richtextbox")
    .WaitForInteractive(richTextBoxButton, maxFrames: 120)
    .Hover(richTextBoxButton)
    .Click(richTextBoxButton)
    .WaitForIdle(InkkOopsIdlePolicy.DiagnosticsStable)
    .Build();

Command composition

This page focuses on how scripts are declared and exposed. For deeper guidance on command families, reduction patterns, pointer-versus-semantic tradeoffs, and proof strategy, read Command patterns.

Pointer commands in authored scripts

Use pointer commands when the bug depends on the actual input path.

return new InkkOopsScriptBuilder("scroll-thumb-drag")
    .Hover("ScrollThumb", InkkOopsPointerAnchor.Center)
    .PointerDown("ScrollThumb", InkkOopsPointerAnchor.Center)
    .Drag("ScrollThumb", 0f, 120f, InkkOopsPointerAnchor.OffsetBy(4f, 4f))
    .PointerUp("ScrollThumb", InkkOopsPointerAnchor.OffsetBy(4f, 124f))
    .Build();

Pointer diagnostics include the final chosen anchor and root-space action point so you can verify what the runtime actually clicked or hovered.

Semantic commands in authored scripts

Use semantic commands when you want stable control-level behavior and do not need pointer fidelity itself.

var sidebar = InkkOopsTargetSelector.Name("CatalogSidebarScrollViewer");
var target = InkkOopsTargetSelector.Within(
    sidebar,
    InkkOopsTargetSelector.AutomationName("RichTextBox"));

return new InkkOopsScriptBuilder("semantic-sidebar-nav")
    .ScrollIntoView(sidebar, target)
    .WaitForInteractive(target)
    .Activate(target)
    .AssertExists("PreviewHost")
    .Build();

Assertions and proof

Assertions prove state. Captures and telemetry prove the visible and structural outcome.

return new InkkOopsScriptBuilder("prove-selection")
    .Click("DataGrid")
    .WaitForVisible("PreviewHost")
    .AssertProperty("SelectionHeader", "Text", "Selected: DataGrid")
    .CaptureFrame("datagrid-selected")
    .DumpTelemetry("datagrid-selected")
    .Build();

Authoring guidelines

  • Prefer selectors over coordinates.
  • Use explicit waits instead of long chains of WaitFrames.
  • Capture proof near the end of the script once the state of interest is stable.
  • Use pointer commands for pointer bugs and semantic commands for control-intent bugs.
  • Keep scripts short enough that a single failed command still tells you something useful.