Guides
Testing
Running a view without a window, as fast as a unit test.
ui.NewTester runs a view without a window, as fast as a unit test: it
renders frames in memory, finds elements by their text, and clicks, types,
scrolls and presses keys.
func TestCounter(t *testing.T) {
app := &counter{}
tt := ui.NewTester(app.view, 320, 240)
if err := tt.Click("Increment"); err != nil {
t.Fatal(err)
}
if app.n != 1 || !tt.HasText("1") {
t.Errorf("count %d, texts %q", app.n, tt.Texts())
}
}Elements are found by their text, or by their Label for those without,
as an icon button: tt.Click("Close"), tt.Find("Volume"),
tt.Focused("Name").
Click, ClickWith and RightClick check the first matching element
before sending input. They return an error if it has no visible area or
the center of that area would reach another control or nothing. They do
not scroll it into view or search for another uncovered point. On an
element without a context menu of its own, as a list's row, RightClick
opens the menu of the content the element holds at that point, as a
right-click there does; a control inside it that takes clicks or input
of its own is another control. Find
still returns a matching clipped box, which may have zero width or height;
ClickAt sends input at the given coordinates without a named target.
Two elements given the same Key under one parent share one state, so
that a click or the focus meant for one goes to the other: a tester
panics where the second key was given, as apps only log it.
What a tester does
-
Pointer.
tt.Click,tt.ClickAt,tt.ClickWith(ui.Shift, "Row 3"),tt.Press,tt.Moveandtt.Release,tt.PressButtonandtt.ReleaseButtonfor another button, astt.PressButton(1, x, y)for a right-button drag,tt.CancelPress, which ends the press as the system cancels one, with no click,tt.Scroll, which scrolls at once as a touchpad does, andtt.Wheel, a notch of a mouse wheel, which glides there as the clock advances (tt.Advance).tt.UsePointer(ui.PointerPen, 0.6)has the presses and moves after it come from a pen pressing that hard, or a finger (ui.PointerTouch). -
Keyboard.
tt.Key(ui.Cmd, ui.KeyS)presses a key,tt.KeyRepeatpresses it again as its auto-repeat does while it is held,tt.TypeKeya key with the text it types, andtt.Typetypes text into the focused element.tt.Composeshows the composition of an input method,tt.Commandperforms an edit command of the menus, such as"copy", andtt.TextCaretreturns where input methods would compose. -
Menus.
tt.RightClickopens a context menu, whichtt.Menulists andtt.ChooseMenuItem("Move to", "Archive")chooses from, andtt.CloseMenucloses: the system's, or one drawn in the window, so a test passes with either. -
The window.
tt.SetSize,tt.SetScale,tt.SetZoom(the content's zoom, asContext.SetZoom; the tester's coordinates, ofFind,ClickAt,MoveandTextCaret, stay the window's),tt.SetDark,tt.SetPreferencesandtt.SetDirection(lay it out right to left) change the window and the desktop's settings;ui.NewTesterWith(view, w, h, ui.TesterOptions{Preferences: p, Dark: true})starts with the desktop's, which the first frame follows, as Media Queries 5 has the user's preferences apply from the first time styles resolve: a view mounted under reduced motion starts no animation in its first frames, whichNewTestersettles before aSetPreferencescan come;tt.SetFocusedtakes the keyboard from the window and gives it back, andtt.SetWindowActivedraws it as inactive and active again, as another window taking the user's input does (inactive windows), andtt.SetWindowHiddenhides all of it from sight and shows it again (hidden windows). -
What it shows.
tt.Texts,tt.HasText,tt.Find(the box of a text),tt.Focused,tt.Cursor,tt.Clipboard,tt.OpenedURLsandtt.Announcements, what the view asked screen readers to read out (live regions);tt.FailOpenURLmakes the links it opens fail, as those no app opens, andtt.FailClipboardits writes of text to the clipboard, as a browser refuses them (WriteClipboardThen). -
What assistive technology sees.
tt.Accessandtt.AccessByRoleread an element's role, name, value and states, andtt.AccessAllevery element's (below). -
Frames.
tt.Framedraws a frame, for what changed outside the view, andtt.Image()is the last frame, for snapshots;ui.Renderdraws a view once at a given scale. Inputs andtt.Framerun the frames the view asks for until it asks for no more, or 20 of them:tt.FramePendingtells whether it still asks, as a view that animates or never settles does. -
Time. A tester has a clock of its own,
tt.Now(), which stands still untiltt.Advance(d)moves it on byd, running the frames due meanwhile at their times: one each refresh while something moves, and those ofc.AfterandPainter.Afterwhen they come due. What depends on time follows it: animations,Context.After,Context.Now, the clicks that make a double click, the typing that undoes in one step, the caret's blinking, tooltips and toasts. So a test reads the same on a busy machine as on a fast one: never sleep to let something happen, advance the clock.Advanceruns only the frames the view asks for: after changing what the view shows from outside it, calltt.Framefirst. The undo steps of aneditor.Bufferand arichtext.Documentuse the system's clock.tt := ui.NewTester(app.view, 320, 240) tt.Click("More") tt.Advance(100 * time.Millisecond) // half way through a 200 ms animation tt.Advance(time.Second) // and done if tt.FramePending() { t.Error("the panel keeps asking for frames") }
Live regions and announcements
tt.Announcements() returns what the view asked screen readers to read
out with c.Announce, and forgets it. To test what live regions read, turn
a screen reader on with tt.UseScreenReader(): from then on every frame's
accessibility tree goes to it, and, as in ARIA, a RoleStatus text that
changes and what is added to a RoleLog are read politely, once, in
tt.Announcements() with the c.Announce texts, while a RoleAlert is
read at once, in tt.Alerts(). What showed before the screen reader was
on is not read, as when a real one attaches.
tt := ui.NewTester(view, 400, 300)
tt.UseScreenReader()
tt.Click("Save")
if got := tt.Announcements(); !slices.Equal(got, []string{"Saved 2 files"}) {
t.Errorf("read out %q", got)
}What assistive technology sees
tt.AccessByRole(role, name) returns what screen readers get of the
first element of the last frame with that role and accessible name (any
name for ""), as Testing Library's getByRole(role, { name }) and
Playwright's find it, and tt.Access(name) the first with that name
whatever its role. The ui.AccessInfo they return has the role, the
name, the value, the description and the RoleDescription, and the
states, as accessibility lists them:
Checked and Mixed (aria-checked, aria-pressed), Selected
(aria-selected, the tab, tree item, row or grid cell chosen, with
Selected or AccessSelected),
Expanded, HasPopup, Busy, Current,
AutoComplete (aria-autocomplete), Sort (a column header's
ui.SortDirection), Disabled, Invalid, Focused (the element or
option the focus is on, ActiveDescendant included), Controls (the
names of the elements it controls, which Access finds), DescribedBy
and ErrorMessage (the names of the elements describing it and holding
its error), Vertical and Horizontal (aria-orientation), Level,
PosInSet and SetSize (aria-posinset, aria-setsize, as
PositionInSet gives them), Multiselectable, RowCount,
ColumnCount, RowIndex and ColumnIndex (of a
grid), RowSpan and ColumnSpan (of its
cells), Min, Max and Now of a range, and its Bounds. So a test
checks
that a state reaches assistive technology, not only that the view set
it:
tt := ui.NewTester(app.view, 320, 240)
reply, ok := tt.AccessByRole(ui.RoleStatus, "Reply")
if !ok || !reply.Busy {
t.Errorf("Reply while streaming: %+v, found %v", reply, ok)
}
if docs, _ := tt.AccessByRole(ui.RoleLink, "Docs"); docs.Current != ui.CurrentPage {
t.Errorf("Docs is not the current page: %+v", docs)
}tt.AccessAll() returns the same of every element of the last frame,
in the order screen readers read them, for checks over a whole view, as
that no control is left without a name:
for _, a := range tt.AccessAll() {
if a.Role == ui.RoleButton && a.Name == "" {
t.Errorf("a button at %v has no name", a.Bounds)
}
}The role is the one the element was given, by its widget or Role, or
the one found from what it does: a text, a button, a group. The tree is
built as screen readers would get it, whether or not one reads the
window, and reading it changes nothing: it builds no frame.
Testing the built app
A tester runs a view inside go test. To test the app as built, with its
real windows, from another process, as an end-to-end test runner or an
agent does, import the automation plugin into
a test build: it reads the elements of every window as a tester's
Find does, by their labels or by their Key, acts on them, sends
input, and waits for frames as FramePending tells.