Skip to content
Testing and Automation

Testing and Automation

gift gives you two tools to check an application without a person in front of it. The package gifttest runs your views inside a go test: it mounts them in a headless application, finds nodes by what they say, clicks them and asserts what happened. The package auto does the same from the outside: built with the tag giftauto, a running application answers HTTP requests on loopback, so a script can read its tree, send input and take screenshots of the real window.

This page explains both, the three build tags that switch on the heavier parts, and the pitfalls that make a UI test lie.

ToolRunsUse it for
gifttestin go test, no window, no GPUbehavior: state, input, focus, layout, overflow
gifttest + giftgpuin go test, renders offscreengolden images and pixel assertions
auto + giftautoinside the running applicationscreenshots, driving the real window from a script

A first test

Take a counter like the one on the state page, with a minus button that is disabled at zero and a key on the label and on each button:

func counter(ctx *gift.Context) gift.View {
	count := ctx.State("count", 0)
	n := ctx.Read(count)

	return ui.Window(ui.VStack(
		ui.Text(strconv.Itoa(n)).Key("count").FontSize(32),
		ui.HStack(
			ui.Button(ui.Text("-"), func() { count.Set(count.Get() - 1) }).
				Key("minus").Frame(56, 44).Disabled(n == 0),
			ui.Button(ui.Text("+"), func() { count.Set(count.Get() + 1) }).
				Key("plus").Frame(56, 44),
		).Gap(8),
	).Gap(16).Padding(24))
}

The test mounts it, clicks the plus button and checks the label:

import (
	"testing"

	"github.com/worldiety/gift/font/inter"
	"github.com/worldiety/gift/gifttest"
	"github.com/worldiety/gift/ui"
)

var font = ui.MustFont(ui.FontQuery{Family: inter.Family})

func TestCounter(t *testing.T) {
	h := gifttest.New(t, gifttest.Options{Root: counter, Font: font})

	h.Find(gifttest.ByKey("count")).AssertText("0")
	h.Find(gifttest.ByKey("minus")).AssertDisabled()

	h.Find(gifttest.ByText("+")).Click()
	h.Find(gifttest.ByKey("count")).AssertText("1")
	h.Find(gifttest.ByKey("minus")).AssertEnabled()
}

The test names no coordinate, no frame and no timer. If you restyle the counter or move the buttons, it keeps passing. gifttest.New builds, lays out and paints the first frame before it returns, and every action settles the application before the next line runs.

Every test needs a font. gift links no typeface into a program that does not ask for one, and a Text without a font panics on its first build. Set Options.Font, as above, or install a default font in your own setup. See Text and Fonts.

Options

gifttest.Options configures the harness:

FieldMeaning
Rootthe root component function, as in gift.Options.Root
Viewa fixed view for a test without state of its own; set exactly one of Root and View
Sizethe viewport in logical pixels; zero means gifttest.DefaultSize, 800 × 600
Fontthe default font for the test; the previous one is restored when the test ends
Themethe color theme for the test, for example ui.DarkTheme(); the zero value keeps the current one
Densitythe device density, rounded like a real monitor’s; zero means 1
MaxFramesthe bound of Settle; zero means gifttest.DefaultMaxFrames, 64
Loggerpassed to gift; nil means no logging

The harness clears every frame to transparent black, like a real window. It paints no background for you. Wrap a component under test in ui.Window when the background matters, for example gifttest.Options{View: ui.Window(card)}.

Selectors

A selector is a predicate over the nodes of the retained tree. These match on what a user or the application can name:

SelectorMatches a node
ByText(s)whose label is exactly s
ByTextContains(sub)whose label contains sub
ByKey(k)whose key, set with .Key(k), is k
ByType(name)built from the view type registered as name, such as "ui.Button"
Interactive()that takes part in hit testing; a disabled control still does
Focusable()that can take the keyboard focus right now
Enabled()that is not disabled
Visible()that is not hidden and not clipped away entirely
Under(parent)below a node that matches parent
Where(desc, pred)for which your own predicate over a Node is true
Any()every node

Combine them with .And, .Or and Not. Under picks one row out of several identical ones:

h.AssertCount(gifttest.ByText("Delete"), 3)
h.Find(gifttest.Under(gifttest.ByKey("row-3")).And(gifttest.ByText("Delete"))).Click()

The label ByText reads is the one the view declares, not the rasterized pixels. A Text declares its string. An icon-only Button has no string anywhere, so give it one with .Label("Close") and the test finds it like a text button.

Four calls run a selector:

  • h.Find(s) returns the one node that matches, and fails unless there is exactly one.
  • h.First(s) returns the first match in document order, and fails if there is none. Use it when several matches are expected and the first is meant.
  • h.FindAll(s) returns every match; an empty result is not a failure.
  • h.Exists(s) reports whether anything matches, without failing.

Node.Find(s) does the same as h.Find, but only within the subtree of that node.

When a selector matches too much, the failure prints the query, the matches and the tree with the matches marked. When it matches nothing, it lists the labels, keys or types that do exist, so a typo stands out. Call h.Dump() yourself to get the tree as text at any point. It shows the type, key, label, layout bounds and input flags of every node, up to 120 lines:

the tree was:
   gift.Component key="root" bounds=(0,0)-(106.578125,90)
     ui.VStack bounds=(0,0)-(106.578125,90)
       ui.HStack key="row-1" bounds=(0,0)-(103.78125,30)
         ui.Text text="row-1" bounds=(0,0)-(37.234375,18)
         ui.Button bounds=(37.234375,0)-(103.78125,30) interactive
           ui.Text text="Delete" bounds=(49.234375,6)-(91.78125,24)
       ui.HStack key="row-2" bounds=(0,30)-(106.15625,60)
       ...

Prefer ByKey when a test has to be certain. A key is under your control and survives a change of wording. It only has to be unique among siblings, though, so combine it with ByType or Under when the same key appears in several places.

Actions

Actions are methods of Node. The harness works out where the node is; you never write the point.

ActionWhat it does
Click()moves the mouse onto the node, presses and releases
Press(), Release()the two halves of a click, for “press here, release there”
Hover()moves the mouse onto the node
Tap()a touch: finger down and up, with no hover
LongPress()holds a finger past gift.LongPressDelay
Drag(dst), DragTo(p)presses, moves in several steps, releases
Swipe(d)a drag with a finger
Fling(d, over)a finger drag that takes over on the injected clock, so it flings
Wheel(d)scrolls the mouse wheel over the node
Focus()moves the keyboard focus to the node; fails if it cannot take it
PressKey(k, mods...)focuses the node and presses one key

A ByText selector usually lands on the text inside a button. The actions climb to the nearest interactive ancestor, so h.Find(gifttest.ByText("+")).Click() clicks the button. Wheel is the exception: it scrolls at the center of the node itself, and gift passes the wheel event up to the nearest scroll container.

The keyboard lives on the harness: h.Tab() and h.ShiftTab() move the focus, h.Key(k, mods...) presses a physical key, and h.TypeText(s) delivers characters to whatever holds the focus. Key(gift.KeySpace) and TypeText(" ") are different things: the first presses the space bar, the second types a space character.

func TestCounterKeyboard(t *testing.T) {
	h := gifttest.New(t, gifttest.Options{Root: counter, Font: font})

	h.Tab()
	h.AssertFocus(gifttest.ByKey("plus"))
	h.Key(gift.KeySpace)
	h.Find(gifttest.ByKey("count")).AssertText("1")
}

A TextField is driven the same way: focus it, type, check what your application did with the text.

func greeting(ctx *gift.Context) gift.View {
	name := ctx.State("name", "")
	ed := ui.Editor(ctx, "name-editor", "")
	return ui.Window(ui.VStack(
		ui.TextField(ed).Bind(name.Binding()).Key("name").Placeholder("Your name"),
		ui.Text("Hello, "+ctx.Read(name)).Key("greeting"),
	).Gap(12).Padding(24))
}

func TestGreeting(t *testing.T) {
	h := gifttest.New(t, gifttest.Options{Root: greeting, Font: font})

	h.Find(gifttest.ByKey("name")).Focus()
	h.TypeText("Ada")
	h.Find(gifttest.ByKey("greeting")).AssertText("Hello, Ada")

	h.Key(gift.KeyBackspace)
	h.Find(gifttest.ByKey("greeting")).AssertText("Hello, Ad")
}

Every action checks where it landed

Before Click, Press, Hover, Tap, LongPress, Drag, DragTo, Swipe or Fling dispatches its event, it hit tests the point it aims at. If the hit test reaches a different node, because an overlay, a later ZStack sibling or a full-size Box covers the target, the action fails and names both nodes. It also fails if the point reaches no interactive node at all. Release and Wheel do not check: a release goes to whatever captured the press anyway. Without this check the covering control would get the click, and the test would fail later with a misleading message about the counter.

Sometimes the overlap is what you want to test, for example that a modal swallows clicks to the screen behind it. Use the coordinate verbs for that: h.ClickAt(p), h.TapAt(p), h.PressAt(p), h.ReleaseAt(p) and h.MoveTo(p) dispatch at a point without a check, and h.At(p) returns the interactive node a click at p would reach, without dispatching anything.

Nodes go stale

A Node holds a reference into the retained tree. When a rebuild replaces the subtree that contained it, the reference goes stale, and every method on it fails with a message that says so. Find the node again after an action that rebuilds it. Holding on to a node across actions is fine as long as it survives, as plus does here:

plus := h.Find(gifttest.ByText("+"))
plus.Click()
plus.Click()

Assertions

Node assertions return the node, so you can chain them:

AssertionFails unless the node
AssertText, AssertKey, AssertTypehas that label, key or view type
AssertEnabled, AssertDisabledis enabled or disabled
AssertFocused, AssertHovered, AssertPressedhas that interaction state; each has an AssertNot… twin
AssertBounds, AssertSizehas that rectangle or size, within half a pixel
AssertVisible, AssertNotVisiblesurvives, or does not survive, the clips above it
AssertDrawsGlyphs, AssertDrawsImagedrew text or a picture in the last frame
AssertBackground, AssertPaintsfilled its shape, or painted an operation, in that color
AssertMaterial, AssertGlassQualitypainted a material of that kind, or a glass of that quality
AssertScrollOffset, AssertAtScrollStart, AssertAtScrollEndhas its scroll container at that position

Harness assertions are about the whole scene: AssertExists, AssertNone and AssertCount take a selector, AssertFocus(s) and AssertNoFocus() check the keyboard focus, and AssertOps counts display list operations that satisfy a predicate.

The state assertions, AssertEnabled to AssertNotPressed, read the nearest interactive node, the node itself or an ancestor, like the actions do. h.Find(gifttest.ByText("+")).AssertDisabled() checks the button around the label; a failure then names the ancestor whose state was read.

AssertNoOverflow fails when any node’s content does not fit the size it reported, and prints the tree. It is the one line every layout test should end with. Combined with Resize, it checks a screen at several window sizes without a second application. Clip(true) does not make it pass: a clip only hides the excess on screen, and the overflow is still reported. Give the container more room or make the content need less.

func TestCounterFitsSmallWindow(t *testing.T) {
	h := gifttest.New(t, gifttest.Options{Root: counter, Size: geom.Sz(320, 240), Font: font})
	h.AssertNoOverflow()

	h.Resize(geom.Sz(200, 160))
	h.AssertNoOverflow()
}

See Layout for what overflow means in gift and Diagnostics for the counters behind it.

Gone or hidden

The tree walk does not stop at hidden subtrees. An inactive tab keeps its views mounted, and a dismissed alert stays in the tree while it animates out. So “the dialog is closed” is not AssertNone(ByText(...)). Ask whether it is still on screen:

h.AssertNone(gifttest.ByText("Reset everything?").And(gifttest.Visible()))

Frames and time

You rarely think about frames. New and every action call h.Settle(), which runs frames until one of them builds nothing and lays out nothing. If the application never settles within MaxFrames, the test fails with the number of builds in the last frame and the tree. The usual cause is a component whose build writes a state that the same build reads.

The clock is injected and starts at zero. Nothing in gifttest sleeps or reads the wall clock. Move the clock with h.Advance(d); LongPress does this for you. h.AdvanceTicks(n, step) moves it in n steps, which is what key repeat needs, because gift delivers a bounded number of repeats per tick. When the number of frames is the subject of the test, call h.Frame() to run exactly one.

Scrolling

Selectors find a node inside a ScrollView even when it is far outside the viewport. The pointer actions that aim scroll it into view first, so a test does not have to. An assertion does not scroll: call ScrollIntoView before AssertVisible. For a test about scrolling itself, use ScrollIntoView, ScrollTo, ScrollBy, Fling and the scroll assertions. Node.Bounds is the rectangle on screen, after scroll offsets; Node.LayoutBounds is the one the layout computed.

Golden images

Everything above runs on the display list, without a GPU. Pixel assertions need real pixels, so they compile against the build tag giftgpu:

  • h.AssertGolden(name) renders the current frame offscreen and compares it with testdata/<name>.png.
  • h.AssertOpaque() fails when any pixel of the frame is not fully opaque.
  • h.AssertPixel(p, c) checks one pixel of the rendered image, within the golden tolerance.
  • h.Image() returns the rendered frame. Without the tag it fails the test instead of skipping.

A golden test pins the font and the theme, and the test package installs gifttest.Main as its TestMain:

func TestMain(m *testing.M) { gifttest.Main(m) }

func TestCounterLooks(t *testing.T) {
	h := gifttest.New(t, gifttest.Options{
		Root:  counter,
		Size:  geom.Sz(240, 180),
		Theme: ui.LightTheme(),
		Font:  ui.MustFont(ui.FontQuery{Family: inter.Family}),
	})
	h.AssertOpaque()
	h.AssertGolden("counter-at-zero")

	h.Find(gifttest.ByText("+")).Click()
	h.AssertGolden("counter-at-one")
}

Run it with the tag. The first run has no reference image yet; it fails and writes what it rendered, so you can look at it before you accept it:

GIFT_UPDATE_GOLDEN=1 go test -tags giftgpu ./...   # write or rewrite testdata/*.png
go test -tags giftgpu ./...                        # compare

Without the tag, golden assertions skip

The same test file compiles in both modes. Under a plain go test ./... AssertGolden, AssertOpaque and AssertPixel skip the test with a message that names the tag, and gifttest.Main is an ordinary m.Run. Set GIFT_REQUIRE_GOLDEN=1 in a CI job that is supposed to check rendering, and a skip becomes a failure.

With the tag, TestMain is required

Ebitengine only reads pixels back inside its run loop. gifttest.Main runs the tests inside that loop; without it, AssertGolden panics. Because it starts Ebitengine, a giftgpu run needs a machine with a display.

A mismatch writes two files

Next to the reference, the failing run writes <name>.actual.png and <name>.diff.png and prints all three paths. The diff is the reference dimmed, with every differing pixel in magenta. Slashes and spaces in the name become underscores.

The comparison allows gifttest.GoldenTolerance, 4 of 255, per channel, to absorb differences between GPU drivers. There is no percentage of pixels that may differ, so a shape that moves by one pixel fails. A rewrite run logs GOLDEN REWRITTEN for every file and verifies nothing.

The image is the viewport times Options.Density in physical pixels, so a test at density 2 checks the high-resolution glyphs and thumbnails. AssertPixel reads the pixel at p in that image, so at a density other than 1 its point is in physical pixels. If the frame shows an Image, call h.Warm() before the golden: the frame that first draws a picture only requests its upload, the texture is ready from the next frame on, and without a warm-up frame the golden would show the placeholder.

Assert that the window is opaque

gift paints only what your views describe, and a real window starts every frame transparent. An application without a background ships a window with holes in it. A golden of such a window is perfectly stable, so it cannot catch the defect. AssertOpaque can. It fails with the number of holes and the first one, and the fix is to wrap the root view in ui.Window.

Pitfalls

  • Do not call t.Parallel in a test that uses gifttest. The text shaping cache, the default font and the theme are process wide, so two parallel tests race on them. Tests in different packages run in separate processes and are not affected.
  • Pin the font and the theme in every test whose result depends on glyph shapes or colors, with Options.Font and Options.Theme. Otherwise the result depends on what another test in the package installed first. If a test changes the default font itself, restore it with t.Cleanup, not defer, so the harness and your test restore in the right order.
  • A Harness belongs to one goroutine, like the gift.App inside it. h.App() gives you the application for anything the harness has no verb for.
  • The harness has one touch pointer and no input method editor; multitouch and CJK composition are out of scope. Umlauts, accents and dead keys arrive as finished characters and work with TypeText.

Automating the real application

gifttest tests your views. The auto package tests the program you ship: a real window, a real frame loop, a real framebuffer. Build the application with the tag giftauto and the Ebitengine backend starts an HTTP server in its Run, just before it opens the window. You add no import and no source file:

go run -tags giftauto ./cmd/example-kitchensink
# in a second terminal:
curl -fsS http://127.0.0.1:7391/tree
curl -fsS 'http://127.0.0.1:7391/screenshot?settle=8' -o screenshot.png
Never ship a build with giftauto. The server has no authentication, can synthesize any input and can read back every pixel. It answers on loopback only and refuses every other peer with 403, but it does not belong on a kiosk. Without the tag, the package compiles to nothing: no import, no goroutine, no server.

The server listens on 127.0.0.1:7391. GIFT_AUTO_ADDR changes the address, and GIFT_AUTO_TIMEOUT changes the time, in milliseconds, that a request waits for the UI goroutine or a frame (default two seconds). A request that times out because nothing is being drawn answers 503.

RouteAnswers with
GET /healthliveness and frame counters, without waiting for the UI goroutine
GET /treethe retained tree as JSON: type, key, text, bounds, centre, visibility, input flags, overflow, scroll state; ?depth=n limits it
GET /querythe matching nodes as a flat list; filters key, text, contains, type, visible=1, interactive=1, limit
GET /screenshota PNG of the framebuffer the window drew; ?settle=n waits for n drawn frames first
POST /inputruns a batch of input steps in order and answers with frame counters
GET /diagthe counters of gift.Diagnostics, the overflowing nodes, the theme, the focused node, viewport and density
POST /themeinstalls a theme: {"mode":"dark"}, or single roles such as {"roles":{"accent":[255,0,255,255]}}

Every request except /health runs its work on the UI goroutine and waits for it, so a shell script that sends one request after another is sequential, not racy. A screenshot shows a frame drawn after the input before it.

Coordinates

All coordinates in and out of the interface are logical pixels, the same space as Node.Bounds in a test. A screenshot is in physical pixels: logical times the density. The answer carries the density in the X-Gift-Density header and the drawn frame number in X-Gift-Frames. Read the centre of a node from /query and aim at it, rather than guessing a point from a screenshot:

curl -fsS 'http://127.0.0.1:7391/query?text=Save&interactive=1' | jq '.nodes[0].centre'

Input steps

A POST /input body is a list of steps. Each step that touches gift runs in its own update, so a press and a release behave like two events, not like one:

{"steps":[
  {"op":"pointerDown","x":100,"y":200},
  {"op":"waitMs","ms":100},
  {"op":"pointerMove","x":100,"y":260},
  {"op":"waitFrames","frames":2},
  {"op":"pointerUp"}
]}
OpsFields
pointerMove, pointerDown, pointerUp, pointerCancel, tapx, y (omitted: the last position), mouse
clickx, y; always a mouse, in one update
wheelx, y, dx, dy in notches; positive is up or left, so "dy":-1 moves a list forward
key, keyDown, keyUpkey such as "tab", "enter", "escape"; mods such as ["shift"]
texttext, typed character by character
focusforward, default true
waitMs, waitFrames, waitTicksms, frames, ticks
captureframes, png
Pointer steps are touches unless you say "mouse":true. The backend polls the physical mouse every tick and moves gift’s mouse pointer to where your real cursor rests. A synthesized mouse press would be dragged there on the next tick, and every button declines a dragged release. A touch is not affected. When you need a mouse, for hover or the focus ring after a click, use the click op, which does the whole gesture inside one update.

Watching something move

A screenshot is a round trip, and the frame it captures is whichever one the window draws next. Two screenshots that look alike do not prove that nothing animated in between. Put a capture step in front of the input instead. It collects the next frames drawn frames, with a hash of each and the number of pixels that changed against the frame before; "png":true includes the images. Motion is a run of non-zero changed counts, and the end of a transition is the run of zeroes after it:

curl -fsS -XPOST http://127.0.0.1:7391/input -d '{"steps":[
  {"op":"capture","frames":16},
  {"op":"tap","x":100,"y":700}]}' | jq '.captured[] | {index,changed}'

The answer to /input also carries animating and animations, the state of animations right after the batch, so a script can wait for the screen to come to rest instead of counting frames. /diag spells the same two fields Animating and Animations.

The screenshots on this site are taken this way. The program in docs/capture builds the gallery with -tags giftauto, starts it once per demo, switches the theme through /theme and fetches /screenshot.

Build tags

TagEffect
giftgpugifttest renders through the Ebitengine backend: golden images and pixel assertions run instead of skipping
giftautothe Ebitengine backend starts the HTTP automation interface on loopback
giftdebuggift checks programming errors that are too expensive for a release build, and panics on them

giftdebug is worth turning on for your test suite, go test -tags giftdebug ./.... With it, gift panics, among other checks, when:

  • state or the gift.App is touched from a goroutine other than the UI goroutine; see Concurrency,
  • two siblings share the same key,
  • the code that built a view keeps writing into the children slice it handed over,
  • a semantic color reaches a painter without being resolved against the theme, which is otherwise a widget that draws nothing at all.

It also names every node that starts or stops overflowing, in the application’s logger or on standard error. Without the tag, these checks compile to nothing. The race detector, go test -race, finds the same class of goroutine bug in a build without the tag.

API

Harness

Constructors

ConstructorDescription
func New(t TB, opts Options) *HarnessNew mounts the view and brings it to a settled, painted steady state.

Methods

MethodDescription
Advance(d time.Duration)Advance moves the injected clock forward by d and settles.
AdvanceTicks(n int, step time.Duration)AdvanceTicks moves the injected clock forward n times by step, opening an input phase and settling at each stop.
App() *gift.AppApp returns the application under test, for the cases this package has no verb for.
AssertCount(s Selector, want int)AssertCount fails unless exactly want nodes match s, listing the ones that did.
AssertExists(s Selector)AssertExists fails when nothing matches s.
AssertFocus(s Selector)AssertFocus fails unless the focused node matches s.
AssertGolden(name string)AssertGolden compares the rendered frame against the PNG in testdata.
AssertNoFocus()AssertNoFocus fails when anything holds the keyboard focus.
AssertNoOverflow()AssertNoOverflow fails when any node’s content does not fit the size it reported.
AssertNone(s Selector)AssertNone fails when anything matches s, naming what did.
AssertOpaque()AssertOpaque fails unless every pixel of the framebuffer is fully opaque.
AssertOps(desc string, want int, pred func(render.Op) bool)AssertOps fails unless exactly want operations of the most recent frame satisfy pred.
AssertPixel(p geom.Point, want render.Color)AssertPixel fails unless the pixel at p is want, within GoldenTolerance.
At(p geom.Point) NodeAt returns the topmost interactive node at the device space point p.
CancelPointer()CancelPointer ends the current mouse press without a release, as the window manager does when the window loses focus.
CancelTouch()CancelTouch ends the current touch without a release.
ClickAt(p geom.Point)ClickAt clicks at a device space point.
Density() float32Density returns the device density in force, after the rounding of gift.RoundDensity.
Diagnostics() gift.DiagnosticsDiagnostics returns the counter snapshot of the application, for the tests whose subject is the frame path itself — “this hover rebuilt nothing” is an assertion about gift.Diagnostics.Builds.
Dump() stringDump returns the retained tree as indented text: type, key, label, bounds and the input flags of every node.
Exists(s Selector) boolExists reports whether at least one node matches s, without failing.
Find(s Selector) NodeFind returns the single node matching s and fails the test when there is not exactly one.
FindAll(s Selector) []NodeFindAll returns every node matching s, in document order.
First(s Selector) NodeFirst returns the first node matching s in document order and fails when there is none.
Focused() (Node, bool)Focused returns the node that currently holds the keyboard focus, and whether anything does.
Frame()Frame advances exactly one frame: input phase, update, paint.
Image() image.ImageImage renders the current frame and returns it.
Key(k gift.Key, mods ...gift.Mods)Key presses and releases one key, with the given modifiers held.
KeyDown(k gift.Key, mods ...gift.Mods)KeyDown presses a key and leaves it down.
KeyUp(k gift.Key, mods ...gift.Mods)KeyUp releases a key.
List() *render.ListList returns the borrowed display list of the most recent frame, for the assertions that need the clip and transform side tables — render.List.Clip and render.List.Glyphs are only meaningful against the list they came from.
MoveTo(p geom.Point)MoveTo moves the mouse to the device space point p.
Now() time.DurationNow returns the value of the injected clock, as a duration since an arbitrary origin.
Ops() []render.OpOps returns a copy of the display list of the most recent frame.
PressAt(p geom.Point)PressAt presses the mouse at p.
Release()Release releases the mouse wherever it currently is.
ReleaseAt(p geom.Point)ReleaseAt releases the mouse at p.
Resize(s geom.Size)Resize changes the viewport and settles.
SetModifiers(mods ...gift.Mods)SetModifiers records which modifier keys are held from now on.
Settle()Settle pumps frames until the application stops changing, and fails if it never does.
ShiftTab()ShiftTab moves the keyboard focus to the previous focusable node.
Size() geom.SizeSize returns the current viewport.
Tab()Tab moves the keyboard focus to the next focusable node, by actually pressing tab.
TapAt(p geom.Point)TapAt is a touch at a device space point: a finger goes down there and comes straight up again.
TouchDownAt(p geom.Point)TouchDownAt puts a finger down at the device space point p and leaves it there.
TouchMoveTo(p geom.Point)TouchMoveTo moves the finger that is down to p.
TouchUpAt(p geom.Point)TouchUpAt lifts the finger at p.
TypeRune(r rune)TypeRune types a single character.
TypeText(s string)TypeText types the string, one character at a time, as a keyboard with the user’s layout would deliver it.
Warm()Warm is a no-op in a build without a graphics context.
Wheel(p geom.Point, d geom.Point)Wheel scrolls at the device space point p by d.

gifttest.Harness on pkg.go.dev ↗

Node

Methods

MethodDescription
AssertAtScrollEnd() NodeAssertAtScrollEnd fails unless the container is at its maximum offset.
AssertAtScrollStart() NodeAssertAtScrollStart fails unless the container is at offset zero.
AssertBackground(want render.Color) NodeAssertBackground fails unless the node fills its shape with want.
AssertBounds(want geom.Rect) NodeAssertBounds fails unless the node’s rectangle equals want within [boundsTolerance], reporting the difference per edge.
AssertDisabled() NodeAssertDisabled fails when the node is not disabled.
AssertDrawsGlyphs() NodeAssertDrawsGlyphs fails when the node’s bounds contain no glyph in the most recent frame.
AssertDrawsImage() NodeAssertDrawsImage fails when the node draws no picture in the most recent frame.
AssertEnabled() NodeAssertEnabled fails when the node is disabled.
AssertFocused() NodeAssertFocused fails when the node does not hold the keyboard focus.
AssertGlassQuality(want render.GlassQuality) NodeAssertGlassQuality fails unless the node’s glass material requested want.
AssertHovered() NodeAssertHovered fails when no mouse is over the node.
AssertKey(want string) NodeAssertKey fails unless the node’s reconciliation key is want.
AssertMaterial(want render.MaterialKind) NodeAssertMaterial fails unless the node paints a material of kind want.
AssertNotFocused() NodeAssertNotFocused fails when the node holds the keyboard focus.
AssertNotHovered() NodeAssertNotHovered fails when the node is hovered.
AssertNotPressed() NodeAssertNotPressed fails when the node is pressed.
AssertNotVisible() NodeAssertNotVisible fails when any part of the node survives the clips above it.
AssertPaints(kind render.OpKind, c render.Color) NodeAssertPaints fails unless at least one operation of the most recent frame is of kind k, covers the node and is drawn in colour c.
AssertPressed() NodeAssertPressed fails when the node is not pressed.
AssertScrollOffset(want float64) NodeAssertScrollOffset fails unless the nearest scroll container at or above the node sits at the document offset want, within [boundsTolerance].
AssertSize(want geom.Size) NodeAssertSize fails unless the node’s size equals want within [boundsTolerance].
AssertText(want string) NodeAssertText fails unless the node’s label is exactly want.
AssertType(want string) NodeAssertType fails unless the node was built from the view type registered under want, such as “ui.Button”.
AssertValue(want string) NodeAssertValue fails unless the value the node’s control reports is exactly want; see Node.Value.
AssertVisible() NodeAssertVisible fails when the node is entirely clipped away by an ancestor, naming the clip that removed it.
Background() (render.Color, bool)Background returns the colour the node fills its shape with in the most recent frame, and whether it fills one at all.
Bounds() geom.RectBounds returns the rectangle of the node in device space: where it actually is on screen, after the scroll offsets and transforms above it.
Center() geom.PointCenter returns the point the pointer actions aim at: the middle of the visible part of the node.
Children() []NodeChildren returns the children of the node, in order.
Click() NodeClick moves the mouse onto the node, presses and releases it.
Describe() stringDescribe returns the one line form of the node used in failure messages: type, key, label and device bounds.
Drag(dst Node) NodeDrag presses the mouse on this node, moves it to the centre of dst in several steps and releases it there.
DragTo(p geom.Point) NodeDragTo presses the mouse on this node, moves it to the device space point p in several steps and releases it there.
Find(s Selector) NodeFind returns the single node in this node’s subtree, including itself, that matches s, and fails when there is not exactly one.
Fling(d geom.Point, over time.Duration) NodeFling is a touch drag along d that takes over milliseconds of the injected clock, so that gift can measure a release velocity and start a kinetic scroll.
Focus() NodeFocus moves the keyboard focus to this node and fails when it cannot take it, which is the honest answer for a disabled or non focusable node.
Glass() render.GlassParamsGlass returns the glass parameters of the node’s material and fails when it has none.
Glyphs() []render.GlyphGlyphs returns the glyphs of the most recent frame that lie inside the node’s bounds, which is how a headless test says “something was actually drawn here” for text.
Hover() NodeHover moves the mouse onto the node without pressing.
Interaction() gift.InteractionInteraction returns the hover, press, focus and disabled state gift keeps for the node.
IsVisible() boolIsVisible reports whether any part of the node survives the clips above it.
IsZero() boolIsZero reports whether n refers to no node at all.
Key() stringKey returns the reconciliation key of the node, or “”.
LayoutBounds() geom.RectLayoutBounds returns the rectangle the layout gave the node, without the transforms of its ancestors.
LongPress() NodeLongPress puts a finger on the node, holds it past gift.LongPressDelay and lifts it.
Material() (render.Material, bool)Material returns the backdrop dependent background of the node in the most recent frame, and whether it has one.
Parent() NodeParent returns the parent node.
Press() NodePress moves the mouse onto the node and presses the button, leaving it down.
PressKey(k gift.Key, mods ...gift.Mods) NodePressKey focuses the node and sends one key press and release to it.
Ref() gift.NodeRefRef returns the underlying gift.NodeRef, for the escape hatch through Harness.App.
Release() NodeRelease releases the mouse button over this node.
ScrollBy(d float64) NodeScrollBy moves the nearest scroll container at or above this node by d document units.
ScrollInfo() gift.ScrollInfoScrollInfo returns the full state of the nearest scroll container at or above this node.
ScrollIntoView() NodeScrollIntoView scrolls every container above this node until it is visible.
ScrollOffset() float64ScrollOffset returns the document offset of the nearest scroll container at or above this node.
ScrollTo(off float64) NodeScrollTo moves the nearest scroll container at or above this node to the document offset off, clamped to its bounds.
Scroller() NodeScroller returns the nearest scroll container at or above this node, and fails when there is none.
Swipe(d geom.Point) NodeSwipe is a drag with a finger: down on this node, along d, up.
Tap() NodeTap is a touch: a finger goes down on the node and comes straight up again.
Text() stringText returns gift.Element.Label of the node, which is what ByText matches against.
Type() stringType returns the registered name of the view type the node was built from, such as “ui.Button”.
Value() stringValue returns what the node’s control holds, as gift.App.NodeValue reports it: the text of a field, “09:30” of a time field, "" for a node that edits no value.
VisibleBounds() (geom.Rect, bool)VisibleBounds returns the part of the node that survives the clips of its ancestors, and whether any of it does.
Wheel(d geom.Point) NodeWheel scrolls over the node by d.

gifttest.Node on pkg.go.dev ↗

Selector

Constructors

ConstructorDescription
func Any() SelectorAny matches every node in the tree.
func ByKey(k string) SelectorByKey matches a node whose reconciliation key is k.
func ByText(s string) SelectorByText matches a node whose label is exactly s.
func ByTextContains(sub string) SelectorByTextContains matches a node whose label contains sub.
func ByType(name string) SelectorByType matches a node built from the view type registered under name, such as “ui.Button” or “ui.Text”.
func Enabled() SelectorEnabled matches a node that is not disabled.
func Focusable() SelectorFocusable matches a node that could currently take the keyboard focus.
func Interactive() SelectorInteractive matches a node that takes part in hit testing, that is one whose element carried a gift.Interactor.
func Not(s Selector) SelectorNot inverts a selector.
func Under(parent Selector) SelectorUnder matches a node that is a descendant of some node matching parent.
func Visible() SelectorVisible matches a node that is actually on the screen: not inside a subtree that declared gift.Element.Hidden, and not entirely clipped away by an ancestor.
func Where(desc string, pred func(Node) bool) SelectorWhere builds a selector from an arbitrary predicate over a Node.

Methods

MethodDescription
And(o Selector) SelectorAnd matches when both selectors match the same node.
Or(o Selector) SelectorOr matches when either selector matches the node.
String() stringString returns the human readable form of the selector, as it appears in a failure message.

gifttest.Selector on pkg.go.dev ↗

Functions

FunctionDescription
func Main(m *testing.M)Main is the TestMain a package with golden tests installs:
func Update() boolUpdate reports whether goldens are being rewritten, that is whether GIFT_UPDATE_GOLDEN is set to something other than “0” or “”.

Related