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.
| Tool | Runs | Use it for |
|---|---|---|
gifttest | in go test, no window, no GPU | behavior: state, input, focus, layout, overflow |
gifttest + giftgpu | in go test, renders offscreen | golden images and pixel assertions |
auto + giftauto | inside the running application | screenshots, 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.
Options.Font, as above, or
install a default font in your own setup. See Text and Fonts.Options
gifttest.Options configures the harness:
| Field | Meaning |
|---|---|
Root | the root component function, as in gift.Options.Root |
View | a fixed view for a test without state of its own; set exactly one of Root and View |
Size | the viewport in logical pixels; zero means gifttest.DefaultSize, 800 × 600 |
Font | the default font for the test; the previous one is restored when the test ends |
Theme | the color theme for the test, for example ui.DarkTheme(); the zero value keeps the current one |
Density | the device density, rounded like a real monitor’s; zero means 1 |
MaxFrames | the bound of Settle; zero means gifttest.DefaultMaxFrames, 64 |
Logger | passed 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:
| Selector | Matches 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.
| Action | What 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:
| Assertion | Fails unless the node |
|---|---|
AssertText, AssertKey, AssertType | has that label, key or view type |
AssertEnabled, AssertDisabled | is enabled or disabled |
AssertFocused, AssertHovered, AssertPressed | has that interaction state; each has an AssertNot… twin |
AssertBounds, AssertSize | has that rectangle or size, within half a pixel |
AssertVisible, AssertNotVisible | survives, or does not survive, the clips above it |
AssertDrawsGlyphs, AssertDrawsImage | drew text or a picture in the last frame |
AssertBackground, AssertPaints | filled its shape, or painted an operation, in that color |
AssertMaterial, AssertGlassQuality | painted a material of that kind, or a glass of that quality |
AssertScrollOffset, AssertAtScrollStart, AssertAtScrollEnd | has 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 withtestdata/<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 ./... # compareWithout 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.Parallelin a test that usesgifttest. 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.FontandOptions.Theme. Otherwise the result depends on what another test in the package installed first. If a test changes the default font itself, restore it witht.Cleanup, notdefer, so the harness and your test restore in the right order. - A
Harnessbelongs to one goroutine, like thegift.Appinside 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.pnggiftauto. 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.
| Route | Answers with |
|---|---|
GET /health | liveness and frame counters, without waiting for the UI goroutine |
GET /tree | the retained tree as JSON: type, key, text, bounds, centre, visibility, input flags, overflow, scroll state; ?depth=n limits it |
GET /query | the matching nodes as a flat list; filters key, text, contains, type, visible=1, interactive=1, limit |
GET /screenshot | a PNG of the framebuffer the window drew; ?settle=n waits for n drawn frames first |
POST /input | runs a batch of input steps in order and answers with frame counters |
GET /diag | the counters of gift.Diagnostics, the overflowing nodes, the theme, the focused node, viewport and density |
POST /theme | installs 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"}
]}| Ops | Fields |
|---|---|
pointerMove, pointerDown, pointerUp, pointerCancel, tap | x, y (omitted: the last position), mouse |
click | x, y; always a mouse, in one update |
wheel | x, y, dx, dy in notches; positive is up or left, so "dy":-1 moves a list forward |
key, keyDown, keyUp | key such as "tab", "enter", "escape"; mods such as ["shift"] |
text | text, typed character by character |
focus | forward, default true |
waitMs, waitFrames, waitTicks | ms, frames, ticks |
capture | frames, png |
"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
| Tag | Effect |
|---|---|
giftgpu | gifttest renders through the Ebitengine backend: golden images and pixel assertions run instead of skipping |
giftauto | the Ebitengine backend starts the HTTP automation interface on loopback |
giftdebug | gift 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.Appis 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
| Constructor | Description |
|---|---|
func New(t TB, opts Options) *Harness | New mounts the view and brings it to a settled, painted steady state. |
Methods
| Method | Description |
|---|---|
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.App | App 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) Node | At 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() float32 | Density returns the device density in force, after the rounding of gift.RoundDensity. |
Diagnostics() gift.Diagnostics | Diagnostics 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() string | Dump returns the retained tree as indented text: type, key, label, bounds and the input flags of every node. |
Exists(s Selector) bool | Exists reports whether at least one node matches s, without failing. |
Find(s Selector) Node | Find returns the single node matching s and fails the test when there is not exactly one. |
FindAll(s Selector) []Node | FindAll returns every node matching s, in document order. |
First(s Selector) Node | First 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.Image | Image 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.List | List 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.Duration | Now returns the value of the injected clock, as a duration since an arbitrary origin. |
Ops() []render.Op | Ops 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.Size | Size 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. |
Node
Methods
| Method | Description |
|---|---|
AssertAtScrollEnd() Node | AssertAtScrollEnd fails unless the container is at its maximum offset. |
AssertAtScrollStart() Node | AssertAtScrollStart fails unless the container is at offset zero. |
AssertBackground(want render.Color) Node | AssertBackground fails unless the node fills its shape with want. |
AssertBounds(want geom.Rect) Node | AssertBounds fails unless the node’s rectangle equals want within [boundsTolerance], reporting the difference per edge. |
AssertDisabled() Node | AssertDisabled fails when the node is not disabled. |
AssertDrawsGlyphs() Node | AssertDrawsGlyphs fails when the node’s bounds contain no glyph in the most recent frame. |
AssertDrawsImage() Node | AssertDrawsImage fails when the node draws no picture in the most recent frame. |
AssertEnabled() Node | AssertEnabled fails when the node is disabled. |
AssertFocused() Node | AssertFocused fails when the node does not hold the keyboard focus. |
AssertGlassQuality(want render.GlassQuality) Node | AssertGlassQuality fails unless the node’s glass material requested want. |
AssertHovered() Node | AssertHovered fails when no mouse is over the node. |
AssertKey(want string) Node | AssertKey fails unless the node’s reconciliation key is want. |
AssertMaterial(want render.MaterialKind) Node | AssertMaterial fails unless the node paints a material of kind want. |
AssertNotFocused() Node | AssertNotFocused fails when the node holds the keyboard focus. |
AssertNotHovered() Node | AssertNotHovered fails when the node is hovered. |
AssertNotPressed() Node | AssertNotPressed fails when the node is pressed. |
AssertNotVisible() Node | AssertNotVisible fails when any part of the node survives the clips above it. |
AssertPaints(kind render.OpKind, c render.Color) Node | AssertPaints 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() Node | AssertPressed fails when the node is not pressed. |
AssertScrollOffset(want float64) Node | AssertScrollOffset fails unless the nearest scroll container at or above the node sits at the document offset want, within [boundsTolerance]. |
AssertSize(want geom.Size) Node | AssertSize fails unless the node’s size equals want within [boundsTolerance]. |
AssertText(want string) Node | AssertText fails unless the node’s label is exactly want. |
AssertType(want string) Node | AssertType fails unless the node was built from the view type registered under want, such as “ui.Button”. |
AssertValue(want string) Node | AssertValue fails unless the value the node’s control reports is exactly want; see Node.Value. |
AssertVisible() Node | AssertVisible 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.Rect | Bounds 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.Point | Center returns the point the pointer actions aim at: the middle of the visible part of the node. |
Children() []Node | Children returns the children of the node, in order. |
Click() Node | Click moves the mouse onto the node, presses and releases it. |
Describe() string | Describe returns the one line form of the node used in failure messages: type, key, label and device bounds. |
Drag(dst Node) Node | Drag presses the mouse on this node, moves it to the centre of dst in several steps and releases it there. |
DragTo(p geom.Point) Node | DragTo presses the mouse on this node, moves it to the device space point p in several steps and releases it there. |
Find(s Selector) Node | Find 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) Node | Fling 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() Node | Focus 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.GlassParams | Glass returns the glass parameters of the node’s material and fails when it has none. |
Glyphs() []render.Glyph | Glyphs 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() Node | Hover moves the mouse onto the node without pressing. |
Interaction() gift.Interaction | Interaction returns the hover, press, focus and disabled state gift keeps for the node. |
IsVisible() bool | IsVisible reports whether any part of the node survives the clips above it. |
IsZero() bool | IsZero reports whether n refers to no node at all. |
Key() string | Key returns the reconciliation key of the node, or “”. |
LayoutBounds() geom.Rect | LayoutBounds returns the rectangle the layout gave the node, without the transforms of its ancestors. |
LongPress() Node | LongPress 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() Node | Parent returns the parent node. |
Press() Node | Press moves the mouse onto the node and presses the button, leaving it down. |
PressKey(k gift.Key, mods ...gift.Mods) Node | PressKey focuses the node and sends one key press and release to it. |
Ref() gift.NodeRef | Ref returns the underlying gift.NodeRef, for the escape hatch through Harness.App. |
Release() Node | Release releases the mouse button over this node. |
ScrollBy(d float64) Node | ScrollBy moves the nearest scroll container at or above this node by d document units. |
ScrollInfo() gift.ScrollInfo | ScrollInfo returns the full state of the nearest scroll container at or above this node. |
ScrollIntoView() Node | ScrollIntoView scrolls every container above this node until it is visible. |
ScrollOffset() float64 | ScrollOffset returns the document offset of the nearest scroll container at or above this node. |
ScrollTo(off float64) Node | ScrollTo moves the nearest scroll container at or above this node to the document offset off, clamped to its bounds. |
Scroller() Node | Scroller returns the nearest scroll container at or above this node, and fails when there is none. |
Swipe(d geom.Point) Node | Swipe is a drag with a finger: down on this node, along d, up. |
Tap() Node | Tap is a touch: a finger goes down on the node and comes straight up again. |
Text() string | Text returns gift.Element.Label of the node, which is what ByText matches against. |
Type() string | Type returns the registered name of the view type the node was built from, such as “ui.Button”. |
Value() string | Value 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) Node | Wheel scrolls over the node by d. |
Selector
Constructors
| Constructor | Description |
|---|---|
func Any() Selector | Any matches every node in the tree. |
func ByKey(k string) Selector | ByKey matches a node whose reconciliation key is k. |
func ByText(s string) Selector | ByText matches a node whose label is exactly s. |
func ByTextContains(sub string) Selector | ByTextContains matches a node whose label contains sub. |
func ByType(name string) Selector | ByType matches a node built from the view type registered under name, such as “ui.Button” or “ui.Text”. |
func Enabled() Selector | Enabled matches a node that is not disabled. |
func Focusable() Selector | Focusable matches a node that could currently take the keyboard focus. |
func Interactive() Selector | Interactive matches a node that takes part in hit testing, that is one whose element carried a gift.Interactor. |
func Not(s Selector) Selector | Not inverts a selector. |
func Under(parent Selector) Selector | Under matches a node that is a descendant of some node matching parent. |
func Visible() Selector | Visible 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) Selector | Where builds a selector from an arbitrary predicate over a Node. |
Methods
| Method | Description |
|---|---|
And(o Selector) Selector | And matches when both selectors match the same node. |
Or(o Selector) Selector | Or matches when either selector matches the node. |
String() string | String returns the human readable form of the selector, as it appears in a failure message. |
Functions
| Function | Description |
|---|---|
func Main(m *testing.M) | Main is the TestMain a package with golden tests installs: |
func Update() bool | Update reports whether goldens are being rewritten, that is whether GIFT_UPDATE_GOLDEN is set to something other than “0” or “”. |
Related
- State: the counter this page tests.
- Input and Focus: what clicks, taps, keys and the focus order do.
- Layout: what overflow is and how to avoid it.
- Diagnostics: the counters behind
Settle,AssertNoOverflowand/diag. - Kiosks: building the binary you ship, without
giftauto. - Counter example and Kitchen sink example.