Skip to content

API reference: package telatest

import "github.com/kuronosu/tela/telatest"

This page is generated from the doc comments of the package; the same text is on pkg.go.dev. For guides and examples, start at the wiki home.

Package telatest tests Tela UIs without a window: it mounts a tree in a headless window, simulates a user with real pointer and key events, finds elements by role and text like Playwright, and compares the pixels with goldens (ARCHITECTURE section 17).

func TestCounter(t *testing.T) {
app := telatest.Mount(t, Counter(), telatest.Size(400, 300))
app.Click(telatest.ByRole("button", "Increment"))
app.ExpectText("Count: 1")
app.Golden("counter.png") // testdata/counter.png; TELA_UPDATE=1 regenerates
}

ByRole, ByText and ByClass describe an element by what a user (or an assistive technology) sees, resolved against the semantics tree each time they are used; ByTestID finds the element that carries an h.TestID, for those a user cannot name (like data-testid, it is not in the aria snapshot). A locator that matches none or several elements makes an action fail with the aria snapshot and the closest names; narrow it with Locator.Filter and Locator.Nth. Hidden elements are never matched.

App.Click (with Right for the secondary button and Delay for a held one), App.DoubleClick, App.Tap, App.LongPress, App.Hover, App.Focus, App.Scroll, App.Drag, App.DragBy, App.Press, App.Type and App.Fill act like a user. The pointer actions move the pointer to the element’s center and send pointer events through hit-testing and the gesture arena (hover and :active states, focus on press, click counts, drags); Tap and LongPress use a touch pointer. The keyboard actions send key events to the focused element (typing into a focused text field; Fill replaces a field’s value like Playwright’s fill).

Before acting, an action waits until the element is visible, enabled, in view and not covered by another element. Before and after, the app is settled: pending state updates are applied and frames run, advancing the app’s clock to each one, until nothing is dirty and no animation or scroll fling is running: transitions (hover:, focus:, dark: changes with transition classes) and finite animations run to their end, so a screenshot after an action shows the settled state. Infinite animations (animate-spin, animate-pulse) are never waited for, and use App.Advance to step them. An app that never goes quiet otherwise (a goroutine that updates state all the time) is not waited for either: the first time it is logged, and actions then run only a few frames. Both are decided in app time (Timeout of it), so a slow machine never cuts a finite animation short.

App.Advance moves the clock by hand: it is how a test waits for a long-press timer, an animation or a fade. To capture the frames of an animation, mount with NoSettle: actions, screenshots and the queries (App.AriaSnapshot, App.Count, App.Bounds, App.Stats, App.Cursor) then leave running animations where they are, and only Advance moves them (ADR-097).

The expectations (App.ExpectText, App.ExpectNoText, App.ExpectRole, App.ExpectCount, App.ExpectFocused, App.ExpectValue, App.ExpectAria and App.Expect) retry until they hold or the Timeout expires, letting a frame of app time and a moment of real time pass between tries, so state set by a goroutine arrives (under NoSettle the first try does not move the clock: it handles only the work due). App.Expect checks the states of an element (Checked, Disabled, Expanded, Selected, Pressed, Focused, HasName, HasValue) without matching strings of the aria snapshot.

App.ClickAt and App.DragFromTo act at window coordinates, for what a locator cannot name; App.Bounds returns an element’s box as a tela.Rect. App.PixelAt reads the frame, App.Measure returns the work the tree did during some actions, and MountFunc is Mount for one component. MountApp mounts a tela.App’s Root in the app’s own window (its size, root class, color scheme, theme and class aliases, and the fields of the OS window: Title, size limits, Frameless, Hidden, Placement, OnClose…), the App that tela.Main runs and tela snapshot reads from the package; FromApp gives the settings that shape what is drawn to Mount, Render and the other entry points.

An app has one window per tela.Window that is mounted, besides its main window. The headless backend plays a fake window manager (ADR-179): it grants every request at once, gives hidden and minimized windows no frames, and records parents, modality, hit regions and caption presses. The app still builds and runs the effects of a hidden or minimized window, as on a desktop (ADR-185): a tray item clicked while the main window is hidden updates its state, its tray menu and the windows it opens; settling waits for that work, and the window draws it when shown.

An App is scoped to one window. Mount returns the one of the main window; App.Windows lists the open windows (in the order they opened, the main window first), App.Window selects one by title, App.WindowAt by index and App.LastWindow is the newest, and App.WaitForWindow runs a step and returns the window it opened, like Playwright’s context.pages() and page.waitForEvent(‘popup’). Each returns an App with the same queries, actions, expectations, goldens and pixels as the main one, working on that window: it is always the same App for the same window.

settings := app.WaitForWindow(func() {
app.Click(telatest.ByRole("button", "Settings"))
})
settings.ExpectText("Preferences")
settings.Golden("settings.png")

Selecting a window waits for it to open, like a locator waits for its element; the clock, settling and the end of the app belong to the whole app, so an action in one window waits for the work it causes in the others. A pointer press or a key in a window that is not the active one activates it first, as the user clicking or typing there does.

The window itself: App.Title, App.WindowState (tela.WindowState, as UseWindow sees it), App.WindowSize, App.Placement, App.Modal, App.Parent, App.Blocks, App.BlockedBy and App.Closed; the expectations App.ExpectTitle, App.ExpectWindowState (with Maximized, Fullscreen, Minimized, Hidden, Active and ClientDecorations), App.ExpectClosed, App.ExpectWindowCount and App.ExpectNoWindow. What the user does with the window manager: App.RequestClose (the close button), App.Minimize, App.Maximize, App.Unmaximize, App.SetFullscreen, App.Restore, App.Move, App.Resize and App.BringToFront. A window blocked by a modal one drops input.

Custom title bars: App.HitRegions and App.HitAt read the regions h.WindowDrag and h.WindowControl send to the OS, a click on the drag area (App.Click, App.ClickAt, App.DoubleClick) is the OS’s press (App.CaptionDrags, App.WindowMenus, a double click maximizes), and the controls are buttons that act on click.

Quitting: App.ExpectQuit waits for tela.Quit or a close request of the main window without an OnClose; the app is then over, as when tela.Main returns: every window unmounted, the cleanups of the effects run, and further use fails the test. App.HasQuit and App.ExpectRunning check that it did or did not. The window options (Title, MinSize, MaxSize, FixedSize, Frameless, AlwaysOnTop, TitleBar, Backdrop, StartHidden, Icon, Placement, OnClose, Screen) set the main window’s fields for Mount.

The window’s clock is manual: it starts at zero and moves only when the app waits or the test advances it, so animations, scroll flings and timers are deterministic. Frames are paced like a 60 Hz display.

App.Golden compares the window’s pixels with testdata/<name>, bit by bit unless a Tolerance is given (Tela renders the same pixels on every platform, ADR-027); App.GoldenScales makes one golden per display scale. TELA_UPDATE=1 go test regenerates goldens (ADR-022). App.AriaSnapshot returns the YAML that tela inspect prints, and App.ExpectAria and App.AriaGolden compare it.

Debug turns on the overlays of TELA_DEBUG (layout, paint-flash, layers, fps, semantics) in a mounted app or a headless render, so a golden shows them. The environment variable itself is ignored: a test only gets overlays it asks for. A paint flash fades over 300 ms of app time and the fps meter redraws on every frame; neither keeps actions waiting, so use App.Advance to look at them, and DebugClock to script the frame times the meter shows.

Render, AriaSnapshot and LayoutJSON mount a tree, run one frame and return the image, the aria snapshot or the layout JSON, for tools. tela snapshot and tela inspect are built on them.

const (
// HitClient is ordinary content: what lies outside every region, and
// what a hole in a drag region is.
HitClient = platform.HitClient
// HitCaption is a drag area (h.WindowDrag): the OS handles presses in
// it (move, double click to maximize, window menu).
HitCaption = platform.HitCaption
// HitMinimize, HitMaximize and HitClose are the window-control buttons
// (h.WindowControl): events over them reach the app as usual.
HitMinimize = platform.HitMinimize
HitMaximize = platform.HitMaximize
HitClose = platform.HitClose
)
const DefaultTimeout = 5 * time.Second

DefaultTimeout is how long actions and expectations wait for their condition when no Timeout is given.

const EnvCheckDamage = "TELA_CHECK_DAMAGE"

EnvCheckDamage is the environment variable that turns CheckDamage on for every app (“1”).

func AriaSnapshot(root tela.Component, opts ...Option) (yaml string, err error)

AriaSnapshot mounts root and returns its semantics tree as a Playwright aria snapshot, without a trailing newline: the YAML tela inspect --aria prints.

Example

yaml, err := telatest.AriaSnapshot(tela.C(Counter))
if err != nil {
panic(err)
}
fmt.Println(yaml)
// Output:
// - 'heading "Count: 0" [level=1]'
// - button "Increment"
func LayoutJSON(root tela.Component, opts ...Option) (js []byte, err error)

LayoutJSON mounts root and returns its layout tree as indented JSON, one object per box with its border box, edges, text and computed style (paragraphs also with the fragments of their inline elements; atomic inlines with inlineBox), ending with a newline: the output of tela inspect --layout. rect is the box as layout placed it; a box that is painted elsewhere, because of a transform or scrolled content, also has bounds, the same shape: the bounding box of its transformed border box in window coordinates, as tela.ElementRef.Bounds returns it.

func Render(root tela.Component, opts ...Option) (img *image.RGBA, err error)

Render mounts root, lays it out and paints it at the configured size and scale (default 800x600 at scale 1) and returns the image, in physical pixels over the root’s background (white by default). Effects run as in a window and then clean up; state passive effects set is rendered only when the tree needs further frames. root is a node or a component, as for Mount; a Prop is an error.

Example

img, err := telatest.Render(Div(Class("size-8 bg-black")),
telatest.Size(16, 16), telatest.Scale(2))
if err != nil {
panic(err)
}
fmt.Println(img.Bounds().Dx(), img.Bounds().Dy(), img.RGBAAt(2, 2))
// Output: 32 32 {0 0 0 255}
func TempDesktopDirs(tb testing.TB) string

TempDesktopDirs points package desktop’s folders (ConfigDir, the settings store, saved window placements…) at a new temporary directory for the rest of the test, through the desktop.DirEnv variable, and returns it. Call it before the app reads a setting. Like t.Setenv, it cannot be used in parallel tests.

type App struct {
// contains filtered or unexported fields
}

App is a UI mounted in a headless window: Tela’s real pipeline and event dispatch with a window that is a record in memory and a clock that only moves when the app waits or the test calls App.Advance.

Actions and expectations settle the app first: they apply the pending state updates and run frames, advancing the clock to each frame, until nothing is dirty and no animation or scroll fling is running. A held pointer’s long-press timer, and the fade of overlay scrollbars, fire only when the clock reaches them, with App.Advance or while an expectation waits.

An App must be used from the goroutine of its test; the state setters and tela.Post may be called from any goroutine.

func Mount(tb testing.TB, root tela.Component, opts ...Option) *App

Mount shows root in a headless window and returns the running app. The window starts focused, so key events reach the focused element; it is unmounted when the test ends, running the cleanups of its effects. root is a node or a component: a struct component value, or a function component through its public wrapper, a function that returns its tela.C or tela.CP node (or use MountFunc).

app := telatest.Mount(t, Counter(), telatest.Size(400, 300))
app := telatest.Mount(t, Card{Title: "Inbox"})

Mount makes the app receive the functions given to tela.Post, which are delivered to one tree per process: do not mount apps that use Post from parallel tests.

The windows a tela.Window opens are opened in the headless backend too, by a fake window manager. The returned App is scoped to the main window; App.Windows, App.Window and App.WaitForWindow give an App scoped to each of the others, with the same queries, actions, snapshots and goldens (see “Windows” in the package documentation).

Example

app := telatest.Mount(t, tela.C(Counter), telatest.Size(400, 300))
app.Click(telatest.ByRole("button", "Increment"))
app.ExpectText("Count: 1")
fmt.Println(app.AriaSnapshot())
// Output:
// - 'heading "Count: 1" [level=1]'
// - button "Increment"
func MountApp(tb testing.TB, app tela.App, opts ...Option) *App

MountApp shows app.Root in a headless window configured like the app, the way tela.Main shows it in a real one, and returns the running app. The size, root class, color scheme, theme, class aliases, scrollbar and scroll settings and Dev of the app apply (see FromApp), and so do the fields of the OS window: Title, AppID, the size limits, FixedSize, Frameless, AlwaysOnTop, Hidden, Icon, Placement and OnClose (see Title, MinSize, Frameless, StartHidden, Placement and OnClose, the options that set them by hand). A Hidden app starts with its window hidden, and without an OnClose the app quits when its main window is asked to close (App.RequestClose, App.ExpectQuit). The options override all of them. It fails the test when app.Root is nil.

var App = tela.App{Title: "Notes", Width: 900, Height: 600, Root: Notes()}
func main() { tela.Main(App) }
func TestNotes(t *testing.T) {
app := telatest.MountApp(t, App) // the same window as main's
app.ExpectText("Notes")
}

tela snapshot and tela inspect read the same package-level var App. See Mount for what the app does and when it ends.

Example

// The App that main passes to tela.Main, and that `tela snapshot` reads
// from the package.
app := telatest.MountApp(t, tela.App{
Title: "Counter",
Width: 480,
Height: 320,
RootClass: "bg-slate-100",
Root: tela.C(Counter),
})
app.Click(telatest.ByRole("button", "Increment"))
app.ExpectText("Count: 1")
fmt.Println(app.Screenshot().Bounds().Size()) // the App's window
// Output: (480,320)
func MountFunc(tb testing.TB, fn func() tela.Node, opts ...Option) *App

MountFunc shows the component fn in a headless window and returns the running app: it is Mount(tb, tela.C(fn), opts…), for the common case of a test that mounts one component.

app := telatest.MountFunc(t, Counter, telatest.Size(400, 300))

See Mount for what the app does and when it ends.

Example

app := telatest.MountFunc(t, Counter, telatest.Size(400, 300))
app.Click(telatest.ByRole("button", "Increment"))
app.ExpectText("Count: 1")
fmt.Println(app.Count(telatest.ByRole("button", "Increment")))
// Output: 1
func (a *App) Advance(d time.Duration)

Advance runs the app’s clock forward by d: the frames, animations, scroll animations and flings, long-press timers and other deadlines that fall within d happen in order, each frame at its own time. It does not settle the app afterwards: work requested at the very end is handled by the next action, expectation or Advance.

Example

// The clock is manual: a long press fires when the app's time gets
// to 500 ms, whatever the machine's speed.
pressed := func() tela.Node {
long, setLong := tela.UseState(false)
return Button(OnLongPress(func(tela.PointerEvent) { setLong.Set(true) }),
IfElse(long, Text("Held"), Text("Press and hold")))
}
app := telatest.Mount(t, tela.C(pressed))
app.Hover(telatest.ByRole("button", ""))
app.MouseDown()
app.Advance(600 * time.Millisecond)
app.MouseUp()
app.ExpectText("Held")
fmt.Println(app.AriaSnapshot())
// Output: - button "Held"
func (a *App) AnswerFileDialog(paths ...string)

AnswerFileDialog answers the oldest open file dialog as the user would: with the given paths, or, without paths, by canceling it. The app’s callback runs and the app settles. It fails the test when no file dialog is open, or when a dialog that takes one path gets several.

app.Click(telatest.ByRole("button", "Open…"))
app.AnswerFileDialog("/home/ana/notes/groceries.md")
app.ExpectText("groceries.md")
func (a *App) AnswerMessage(answer desktop.Answer)

AnswerMessage answers the oldest open message box with the button answer, as the user would (a drawn box gets a real click on its button); the app’s callback runs and the app settles. It fails the test when no box is open or the box has no such button.

app.Click(telatest.ByRole("button", "Close"))
app.AnswerMessage(desktop.AnswerNo) // "Save the changes?" — No
func (a *App) AriaGolden(name string)

AriaGolden compares the app’s aria snapshot, with a final newline (what tela inspect prints), with the text file testdata/<name> (TELA_UPDATE=1 writes it). Use it for snapshots too long to write in the test; App.ExpectAria is better for short ones.

func (a *App) AriaSnapshot() string

AriaSnapshot returns the app’s semantics tree as a Playwright aria snapshot, the YAML that tela inspect prints, without a trailing newline, after settling the app (under NoSettle, handling only the work due: running animations stay where they are). For a counter:

snap := app.AriaSnapshot()
// - heading "Count: 1" [level=1]
// - button "Increment"
func (a *App) Backdrop() tela.Backdrop

Backdrop returns the system backdrop the window has asked the OS for (tela.App.Backdrop, tela.Window.Backdrop).

func (a *App) Blocked() bool

Blocked reports whether a visible modal window blocks this one; see App.BlockedBy.

func (a *App) BlockedBy() *App

BlockedBy returns the visible modal window that blocks this one, or nil. A blocked window takes no input: core drops it (after cancelling the pointers) and a press in it activates the modal window, and a request to close it is refused.

func (a *App) Blocks() *App

Blocks returns the window a modal window blocks while it is visible: its parent. It is nil for a window that is not modal, or hidden.

func (a *App) Bounds(l Locator) tela.Rect

Bounds returns the border box of the element l locates, in logical pixels from the window’s top-left corner, after transforms and scroll offsets: the same rectangle as tela.ElementRef.Bounds. For an inline element it bounds its text.

r := app.Bounds(telatest.ByRole("button", "Save"))
if r.Width < 80 { t.Errorf("the button is %gpx wide", r.Width) }
func (a *App) BringToFront()

BringToFront activates the window as the user does by clicking its task bar entry: it comes back from minimized and, when it is visible, takes the keyboard focus from the other windows. The pointer and key actions do it by themselves, as clicking or typing in a window does. Playwright’s page.bringToFront().

func (a *App) CaptionDrags() int

CaptionDrags returns how many presses in a caption region started a move of the window by the fake window manager: a primary press that is not the second of a double click. A press in a caption region is the OS’s: the app does not see it or the rest of its pointer sequence. Drive one with App.Click on an element inside an h.WindowDrag region, or App.ClickAt.

func (a *App) Click(l Locator, opts ...ClickOption)

Click moves the pointer to the center of the element l locates, presses the primary button and releases it: real pointer events (hover, :active, focus on press, the gesture arena and the click), not a call to the handler. Each Click is a first click (ClickCount 1) even right after another, as in Playwright; use App.DoubleClick for two. It waits for the element to be visible, enabled, in view and not covered by another element, and fails the test when l matches none or several. With Right it presses the secondary button instead (the element’s OnContextMenu runs, not its OnClick); App.Tap and App.LongPress are the touch counterparts.

func (a *App) ClickAt(x, y float32, opts ...ClickOption)

ClickAt clicks at the point (x, y) of the window, in logical pixels from its top-left corner, with the same real pointer events as App.Click (hover, :active, focus on press, the gesture arena and the click) but without looking for an element: whatever is under the point receives them, like a user clicking there. It is for what a locator cannot name (a canvas, a chart, a drop zone found with App.Bounds) and fails the test when the point is outside the window. Right clicks with the secondary button, to open a context menu at a point.

r := app.Bounds(telatest.ByTestID("chart"))
app.ClickAt(r.X+r.Width/2, r.Y+10)
app.ClickAt(r.X+20, r.Y+20, telatest.Right())

Example

app := telatest.MountFunc(t, Counter, telatest.Size(400, 300))
r := app.Bounds(telatest.ByRole("button", "Increment"))
app.ClickAt(r.X+r.Width/2, r.Y+r.Height/2)
app.ExpectText("Count: 1")
fmt.Println("clicked")
// Output: clicked
func (a *App) ClickNotification(title string)

ClickNotification clicks the open notification with the given title (the newest, if several): its OnClick runs and it closes.

func (a *App) ClickNotificationAction(title, action string)

ClickNotificationAction clicks the button labeled action of the open notification with the given title: the action’s OnClick runs and the notification closes.

func (a *App) ClickTray()

ClickTray clicks the app’s tray icon (its OnClick runs). It fails the test unless the app shows exactly one.

func (a *App) ClickTrayItem(path ...string)

ClickTrayItem chooses an item of the tray icon’s menu (its OnSelect runs), named by its label, and for an item in a submenu by the labels of the submenus first:

app.ClickTrayItem("Quit")
app.ClickTrayItem("Theme", "Dark")

It fails the test when the app does not show exactly one tray icon, or the item is missing, disabled, or opens a submenu.

func (a *App) Close()

Close unmounts the whole app now (every window, not only the one this App is scoped to), running the cleanups of its effects. The test does it when it ends; call it to test unmounting. Using the app afterwards fails the test. To test what the user closing a window does, use App.RequestClose.

func (a *App) Closed() bool

Closed reports whether the window is closed: it unmounted (its tela.Window left the tree, or the user’s close request was granted) or the app ended.

func (a *App) Count(l Locator) int

Count returns how many elements l matches now, after settling the app (under NoSettle, handling only the work due: running animations stay where they are). It does not wait.

func (a *App) Cursor() string

Cursor returns the CSS name of the mouse cursor the window shows (“default”, “pointer”, “not-allowed”…), after settling the app (under NoSettle, handling only the work due).

func (a *App) DismissNotification(title string)

DismissNotification closes the open notification with the given title as the user does with its close button (or as it expires): its OnClose runs.

func (a *App) DoubleClick(l Locator)

DoubleClick clicks the element twice in a row, so the second click has a click count of 2 and OnDoubleClick runs. See App.Click.

func (a *App) Drag(from, to Locator, opts ...DragOption)

Drag presses the primary button at the center of from, moves the pointer in steps to the center of to, and releases it there: a real drag through the gesture arena (OnDragStart, OnDrag, OnDragEnd), the pointer capture and the hover states of the elements on the way. The center of to is read once the drag has started.

func (a *App) DragBy(l Locator, dx, dy float32, opts ...DragOption)

DragBy drags the element l locates by (dx, dy) logical pixels from its center. See App.Drag.

func (a *App) DragFromTo(from, to tela.Point, opts ...DragOption)

DragFromTo presses the primary button at the point from, moves the pointer in steps to the point to and releases it there, in logical pixels from the window’s top-left corner: the drag of App.Drag without locators, for sources and targets a locator cannot name (a canvas, a slider track at a given value). It fails the test when a point is outside the window.

r := app.Bounds(telatest.ByTestID("track"))
app.DragFromTo(tela.Point{X: r.X + 4, Y: r.Y + 8}, tela.Point{X: r.X + r.Width/2, Y: r.Y + 8})

Example

moved := 0
area := func() tela.Node {
return Div(Class("size-32 bg-slate-200"),
OnDrag(func(e tela.DragEvent) { moved = int(e.OffsetX) }))
}
app := telatest.MountFunc(t, area, telatest.Size(200, 200))
app.DragFromTo(tela.Point{X: 10, Y: 10}, tela.Point{X: 90, Y: 10})
fmt.Println(moved)
// Output: 80
func (a *App) Expect(l Locator, states ...State)

Expect waits until the element l locates satisfies every state, like Playwright’s toBeChecked, toBeDisabled and toBeExpanded: it settles the app and checks, and while a state does not hold lets a frame of app time pass and checks again, until it holds or the Timeout expires. The locator must match exactly one element (strictness). The failure message says which states differ, shows the element’s actual states and its line of the aria snapshot.

app.Expect(telatest.ByRole("switch", "Wi-Fi"), telatest.Checked(true), telatest.Disabled(false))
app.Expect(telatest.ByRole("button", "Menu"), telatest.Expanded(true))
app.Expect(telatest.ByRole("textbox", "Email"), telatest.HasValue("ada@example.com"))

Example

app := telatest.MountFunc(t, Settings, telatest.Size(400, 400))
wifi := telatest.ByRole("switch", "Wi-Fi")
app.Expect(wifi, telatest.Checked(false))
app.Click(wifi)
app.Expect(wifi, telatest.Checked(true), telatest.Disabled(false))
app.Expect(telatest.ByRole("button", "Save"), telatest.Disabled(true))
fmt.Println("ok")
// Output: ok
func (a *App) ExpectAria(yaml string)

ExpectAria waits until the window’s aria snapshot equals yaml (see App.AriaSnapshot), like Playwright’s toMatchAriaSnapshot in exact mode: after trimming the surrounding blank lines and removing the indentation common to all lines, so a raw string can be indented like the code around it.

app.ExpectAria(`
- heading "Count: 1" [level=1]
- button "Increment"
`)

Example

app := telatest.Mount(t, tela.C(Counter))
app.Click(telatest.ByText("Increment"))
// The expected snapshot may be indented like the code around it.
app.ExpectAria(`
- 'heading "Count: 1" [level=1]'
- button "Increment"
`)
fmt.Println("matches")
// Output: matches
func (a *App) ExpectClosed()

ExpectClosed waits until the window is closed (see App.Closed) and fails the test at the timeout. A close request the app refuses (a tela.Window whose OnClose does not update the state that renders it) leaves the window open.

func (a *App) ExpectCount(l Locator, n int)

ExpectCount waits until l matches exactly n elements.

func (a *App) ExpectFocused(l Locator)

ExpectFocused waits until the element l locates has the keyboard focus.

func (a *App) ExpectNoText(text string)

ExpectNoText waits until the window does not show text (see App.ExpectText).

func (a *App) ExpectNoWindow(title string)

ExpectNoWindow waits until no open window has the title, and fails the test at the timeout.

func (a *App) ExpectQuit()

ExpectQuit waits until the app quits (see App.HasQuit) and fails the test at the timeout. Once the app quit, its windows are unmounted and the cleanups of their effects have run, so the test can assert them.

app.Click(telatest.ByRole("button", "Quit")) // OnClick(tela.Quit)
app.ExpectQuit()
if !cleaned { t.Error("the effect did not clean up") }
func (a *App) ExpectRole(role, name string)

ExpectRole waits until an element with the role and accessible name is visible (see ByRole; an empty name accepts any). It passes when one or more elements match: use App.ExpectCount to assert there is exactly one.

app.ExpectRole("heading", "Settings")
func (a *App) ExpectRunning()

ExpectRunning fails the test when the app quit (see App.HasQuit): the check of an OnClose that hides the window to keep the app in the tray.

func (a *App) ExpectText(text string)

ExpectText waits until the window shows text: it is contained in the window’s text content (whitespace normalized, hidden elements left out, block boundaries counting as spaces; case sensitive).

app.ExpectText("Count: 1")
func (a *App) ExpectTitle(title string)

ExpectTitle waits until the window’s title is title.

func (a *App) ExpectValue(l Locator, value string)

ExpectValue waits until the text field l locates holds value (Playwright’s toHaveValue).

app.Type("hello")
app.ExpectValue(telatest.ByRole("textbox", "Message"), "hello")
func (a *App) ExpectWindowCount(n int)

ExpectWindowCount waits until the app has n open windows (the main window counts), and fails the test at the timeout.

func (a *App) ExpectWindowState(conds ...WindowCond)

ExpectWindowState waits until the window’s state meets every condition, and fails the test at the timeout, showing the unmet conditions and the state it has.

win.Maximize()
win.ExpectWindowState(telatest.Maximized(true), telatest.Minimized(false))
func (a *App) FileDialogs() []FileDialog

FileDialogs returns the file dialogs open now, oldest first, after settling the app.

func (a *App) Fill(l Locator, text string)

Fill fills the text field l locates (an h.Input or h.Textarea) with text, like Playwright’s fill: after the actionability checks of the pointer actions, it focuses the field, selects its value and replaces it with text as a single insertText edit (an empty text deletes the value), firing OnBeforeInput and OnChange once. MaxLength applies. It fails the test when the element is not an editable text field.

app.Fill(telatest.ByRole("textbox", "Email"), "ana@example.com")

Use App.Type to send the characters one key at a time instead.

Example

form := func() tela.Node {
name, setName := tela.UseState("")
return Div(
Label(For("name"), Text("Name")),
Input(ID("name"), Bind(name, setName)),
P(Textf("Hello, %s!", name)),
)
}
app := telatest.Mount(t, tela.C(form))
app.Fill(telatest.ByRole("textbox", "Name"), "Ada")
app.ExpectValue(telatest.ByRole("textbox", "Name"), "Ada")
app.ExpectText("Hello, Ada!")
fmt.Println(app.Count(telatest.ByText("Hello, Ada!")))
// Output: 1
func (a *App) Focus(l Locator)

Focus gives the keyboard focus to the element l locates, like calling focus() on it: OnBlur and OnFocus run. It fails the test when the element cannot take the focus (give it a Button, TabIndex or an interactive role element).

func (a *App) Golden(name string, opts ...GoldenOption)

Golden compares the window’s content, after settling the app (with NoSettle, at the current time: see App.Screenshot), with the PNG testdata/<name> next to the test (a name without the “.png” extension gets it), bit by bit unless a Tolerance is given. TELA_UPDATE=1 writes the golden instead of comparing (ADR-022); review the PNG before committing it. On a mismatch, <name>_actual.png and <name>_diff.png are written next to the golden.

The picture is physical pixels, so a golden depends on Size and Scale; it also shows the overlay scrollbars while they are visible: advance the clock to hide them first when that matters.

func (a *App) GoldenScales(name string, scales ...float64)

GoldenScales compares the window with one golden per display scale: testdata/<name>_x<scale>.png, with the scale written like %g (name_x1.png, name_x2.png, name_x1.5.png), the naming of the examples’ goldens. A name with the “.png” extension has it removed first. For each scale the window is moved to a display with that scale, as when a user drags it between monitors (the logical size is kept, the picture is size*scale pixels), settled like App.Golden does, and compared; the app keeps its state and its original scale at the end. The comparison is bit by bit and TELA_UPDATE=1 writes the goldens, as with App.Golden.

app.GoldenScales("settings", 1, 2) // testdata/settings_x1.png, settings_x2.png

Example

GoldenScales has no output to check: it compares files in testdata.

app := telatest.MountFunc(t, Counter, telatest.Size(400, 300))
// testdata/counter_x1.png and testdata/counter_x2.png; TELA_UPDATE=1
// writes them.
app.GoldenScales("counter", 1, 2)
func (a *App) HasQuit() bool

HasQuit reports whether the app quit: tela.Quit ran, or the main window was asked to close without a tela.App.OnClose. A quit app is over, as when tela.Main returns: every window was unmounted, so the cleanups of the effects ran, and using the app fails the test. The app is settled first.

func (a *App) HitAt(x, y float32) HitKind

HitAt returns what the OS would find at the point (x, y), in logical pixels from the window’s top-left corner: the kind of the last hit region that contains it, or HitClient.

func (a *App) HitRegions() []HitRegion

HitRegions returns the hit regions the window sent to the OS, in order (later regions are on top): the drag areas, the holes in them and the window-control buttons that h.WindowDrag, h.WindowNoDrag and h.WindowControl mark. It is nil for a window without them.

func (a *App) Hover(l Locator)

Hover moves the pointer to the center of the element l locates, so its hover: and group-hover: states apply. It waits for the element to be visible, in view and not covered.

func (a *App) LastWindow() *App

LastWindow returns the newest open window: the one opened last, or the main window when it is the only one.

func (a *App) LayoutJSON() []byte

LayoutJSON returns the layout tree of the running app as indented JSON, ending with a newline, after settling it (under NoSettle, handling only the work due): the output of tela inspect --layout and of the function LayoutJSON, for the app as it is now, with the state its actions built up.

func (a *App) LayoutJSONOf(l Locator) []byte

LayoutJSONOf returns the layout subtree of the element l locates, in the same JSON as App.LayoutJSON (indented, ending with a newline, rects in window coordinates before scroll offsets and transforms, with the painted “bounds” where they differ): the box of the element with everything inside it, as tela inspect --layout shows it at that place. An inline element, a text or a display: contents element gives the box of its nearest ancestor that has one. Like the other locator queries it settles the app first (under NoSettle, handling only the work due), and fails the test when l matches no element or several. tela mcp’s inspect tool is built on it (ADR-194).

js := app.LayoutJSONOf(telatest.ByRole("button", "Save"))
func (a *App) LongPress(l Locator)

LongPress presses the element l locates with a finger at its center, holds it past the long-press threshold (500 ms of app time: the clock moves, so the timers run) and lifts it. The element’s OnLongPress runs while the finger is down and the press then ends without a click. A touch long press also opens the menu of a kit.ContextMenu or aria.UseContextMenuTrigger (a long press of the mouse does not; see App.Click with Delay). It waits for the element to be visible, enabled, in view and not covered.

app.LongPress(telatest.ByTestID("photo"))
app.ExpectCount(telatest.ByRole("menu", "Actions"), 1)
func (a *App) MainWindow() *App

MainWindow returns the App scoped to the app’s main window, the one Mount returned.

func (a *App) Maximize()

Maximize maximizes the window to the fake screen (Screen) as the user does with the OS; it also brings it back from minimized.

func (a *App) Measure(f func()) Stats

Measure runs f, which performs actions on the app, settles the app afterwards like an action does (with NoSettle, only the work due is handled) and returns the work the tree did meanwhile: the difference of two readings of App.Stats, so that a test can assert how narrow an update was without keeping the readings itself. The app is brought to rest before f runs, so work pending from earlier steps is not counted.

st := app.Measure(func() { app.Click(telatest.ByRole("button", "Increment")) })
if st.Builds != 1 { t.Errorf("the click rebuilt %d components, want 1", st.Builds) }

Example

app := telatest.MountFunc(t, Counter, telatest.Size(400, 300))
st := app.Measure(func() {
app.Click(telatest.ByRole("button", "Increment"))
})
fmt.Println("builds:", st.Builds, "frames:", st.Frames > 0)
// Output: builds: 1 frames: true
func (a *App) MessageBoxes() []MessageBox

MessageBoxes returns the message boxes open now, oldest first (native ones before drawn ones), after settling the app.

func (a *App) Minimize()

Minimize minimizes the window, as the user does with the OS: it loses the focus and gets no frames until it is restored or activated.

func (a *App) Modal() bool

Modal reports whether the window is modal to the window it was declared in (tela.Window.Modal).

func (a *App) MouseDown()

MouseDown presses the primary button at the pointer’s position (move it first with MouseMove, Hover or Click).

func (a *App) MouseMove(x, y float32)

MouseMove moves the pointer to (x, y), in logical pixels from the window’s top-left corner.

func (a *App) MouseUp()

MouseUp releases the primary button.

func (a *App) Move(x, y int)

Move moves the window so that its content area’s top-left corner is at (x, y) physical pixels of the fake desktop, as the user dragging it. The position shows in App.Placement from then on.

func (a *App) Notifications() []Notification

Notifications returns the notifications the app showed, oldest first, closed ones included, after settling the app.

func (a *App) Now() time.Duration

Now returns the app’s clock: the time since the window was created, as moved by waits and App.Advance.

func (a *App) Parent() *App

Parent returns the window this one was declared in (the App of the window whose component rendered the tela.Window), or nil for the main window.

func (a *App) PixelAt(x, y float32) color.RGBA

PixelAt returns the color of the window’s current frame at the point (x, y), in logical pixels from the top-left corner. The frame is in device pixels, so the sampled one is the device pixel that contains the point: (floor(x*scale), floor(y*scale)), where scale is the window’s display scale; at scale 2, PixelAt(10, 10) reads the pixel (20, 20), the top-left one of the four that cover the logical pixel. The app is settled first (with NoSettle, only the work due is handled). The color is as stored in the frame (alpha-premultiplied; windows are opaque, so A is 255). It fails the test when the point is outside the window.

if c := app.PixelAt(40, 20); c != (color.RGBA{R: 59, G: 130, B: 246, A: 255}) {
t.Errorf("the button is %v, want blue-500", c)
}

Example

swatch := func() tela.Node {
return Div(Class("size-4 bg-[#ff0000]"))
}
// At scale 2 the logical point (8, 8) is the device pixel (16, 16).
app := telatest.MountFunc(t, swatch, telatest.Size(32, 32), telatest.Scale(2))
fmt.Println(app.PixelAt(8, 8), app.PixelAt(20, 20))
// Output: {255 0 0 255} {255 255 255 255}
func (a *App) Placement() tela.WindowPlacement

Placement returns where the window is (tela.WindowHandle.Placement): its size outside maximized and fullscreen, its position once the app or App.Move set one, the state to open in. It still answers after the window closed.

func (a *App) Press(combo string)

Press presses and releases a key, sending the events to the focused element (or, when nothing is focused, to the outermost element of the content). The syntax is Playwright’s: a key name or character, with modifiers joined by “+”:

app.Press("Enter")
app.Press("Shift+Tab")
app.Press("Control+s") // ControlOrMeta+s on the platform's shortcut key
app.Press("ArrowDown")
app.Press("a") // the character "a"

Each key is a press and a release with the physical Code, the logical Key, the Text it produces (none for Enter, arrows…), its Location and the modifier state, in the US layout; modifiers are pressed first, as left keys, and released last.

Example

search := func() tela.Node {
query, setQuery := tela.UseState("")
submitted, setSubmitted := tela.UseState("")
return Form(OnSubmit(func(tela.SubmitEvent) { setSubmitted.Set(query) }),
Input(AriaLabel("Search"), Bind(query, setQuery)),
P(Textf("Searched: %q", submitted)),
)
}
app := telatest.Mount(t, tela.C(search))
app.Focus(telatest.ByRole("textbox", "Search"))
app.Type("tela") // one key at a time
app.Press("Enter") // submits the form
app.ExpectText(`Searched: "tela"`)
fmt.Println("ok")
// Output: ok
func (a *App) RequestClose()

RequestClose asks to close the window as the user does with the close button of its title bar or Alt+F4: the window’s OnClose runs (for the main window tela.App.OnClose, or the app quits). Whether the window closes is up to the app: use App.ExpectClosed, App.ExpectQuit or App.ExpectWindowCount to assert it. A blocked window refuses the request and the modal window above it is activated. To press the close button of a custom title bar, click it (App.Click).

func (a *App) Resize(width, height float32)

Resize changes the logical size of the window, in pixels, as a user resizing it: the app keeps its state, the breakpoint variants (md:, max-lg:) and the layout follow the new size, and the app is settled afterwards (with NoSettle, only the work due is handled). The display scale is kept; App.SetScale changes it. It fails the test for a size that is not positive. tela mcp’s resize tool is built on it (ADR-174).

app.Resize(390, 844) // a phone-sized window
app.ExpectCount(telatest.ByRole("button", "Open menu"), 1)
func (a *App) Restore()

Restore brings the window back to its normal state: not minimized, not maximized and not fullscreen.

func (a *App) Screenshot() *image.RGBA

Screenshot returns a copy of the window’s content, in physical pixels (the logical size times the scale), after settling the app; with NoSettle, the content at the current time, running animations where they are.

func (a *App) Scroll(l Locator, dx, dy float32)

Scroll moves the pointer to the center of the element l locates and scrolls the wheel there by (dx, dy) logical pixels, positive to the right and down, as a touchpad does: the nearest scroll container under the pointer that can move scrolls at once, with no animation. Use Press(“PageDown”) or Press(“End”) for the animated key scrolling.

func (a *App) SetFullscreen(on bool)

SetFullscreen makes the window fullscreen on the fake screen, or leaves fullscreen; entering also brings it back from minimized.

func (a *App) SetScale(scale float64)

SetScale moves the window to a display with another scale (device pixels per logical pixel), as a user dragging it between monitors: the logical size and the state are kept and screenshots become width*scale x height*scale pixels. The app is settled afterwards (with NoSettle, only the work due is handled). It fails the test for a scale that is not positive. App.GoldenScales compares goldens at several scales without keeping the change.

app.SetScale(2)
func (a *App) Stats() Stats

Stats returns the work counters of the app’s tree, after settling it (under NoSettle, handling only the work due).

func (a *App) Tap(l Locator)

Tap taps the element l locates with a finger: it puts a touch pointer down at the element’s center and lifts it, with real touch pointer events (no hover; :active, the gesture arena and the click as for a finger), like Playwright’s tap. The element’s OnClick runs, with a touch pointer in its event. It waits for the element to be visible, enabled, in view and not covered, and fails the test when l matches none or several.

app.Tap(telatest.ByRole("button", "Save"))
func (a *App) Title() string

Title returns the window’s title, as the OS shows it: tela.App.Title or tela.Window.Title, as of the latest build.

func (a *App) TitleBar() tela.TitleBarColors

TitleBar returns the title bar colors the window has asked the OS for (tela.App.TitleBar, tela.Window.TitleBar), at the 8-bit precision the OS takes; a color that is the system’s is the zero Color.

func (a *App) Tray() TrayIcon

Tray returns the app’s tray icon; it fails the test unless the app shows exactly one.

func (a *App) Trays() []TrayIcon

Trays returns the tray icons the app shows now, in the order they were added, after settling the app.

func (a *App) Type(text string)

Type types text one character at a time, as key presses and releases sent to the focused element: each has the character’s physical Code (US layout, when it has one), Key and Text, and no modifiers (an upper case letter is not typed with Shift, as in Playwright’s keyboard.type). A newline types Enter and a tab Tab. In a focused text field each key inserts its text through the real key path (OnKeyDown, then OnBeforeInput and OnChange), as a user typing; App.Fill replaces the value at once.

func (a *App) Unmaximize()

Unmaximize restores a maximized window to its size.

func (a *App) Unsupported(services ...DesktopService)

Unsupported makes the headless desktop lack the given services from now on, as a desktop without them does (Linux without a portal, GNOME without a tray): the calls return errors wrapping desktop.ErrUnsupported, and ShowMessage draws its box. Called with nothing, every service is supported again.

app := telatest.Mount(t, Editor{})
app.Unsupported(telatest.FileDialogs)
app.Click(telatest.ByRole("button", "Open…"))
app.ExpectText("Type the path of the file")
func (a *App) WaitFor(what string, cond func() bool)

WaitFor waits until cond reports true, settling the app and letting a frame of app time and a moment of real time pass between tries, until the timeout (see Timeout); under NoSettle the first try handles only the work due, and the clock moves only between failed tries. It is for conditions the locators cannot see: a value computed from App.Bounds, or Go state that a goroutine sets. what names the condition in the failure message (“the progress bar reached 3 s”). cond runs on the test goroutine.

func (a *App) WaitForWindow(open func()) *App

WaitForWindow runs open, which should make the app open a window (a click, a key press, a goroutine started by one), and returns the App of the window that opened as a result, waiting for it: Playwright’s page.waitForEvent(‘popup’). When several windows opened it returns the first of them. It fails the test at the timeout.

settings := app.WaitForWindow(func() {
app.Click(telatest.ByRole("button", "Settings"))
})
settings.ExpectText("Preferences")

Example

var clicks int
var cleaned []string
app := mountNotes(t, &clicks, &cleaned)
settings := app.WaitForWindow(func() {
app.Click(telatest.ByRole("button", "Settings"))
})
settings.ExpectText("Preferences")
settings.Click(telatest.ByRole("button", "Applied 0"))
settings.ExpectText("Applied 1")
fmt.Println(settings.Title(), settings.Modal(), app.Blocked(), len(app.Windows()))
// Output: Settings true true 2
func (a *App) Window(title string) *App

Window returns the open window whose title is title (exactly), waiting until there is one, as Playwright waits for a locator. It fails the test at the timeout, listing the titles of the open windows, and at once when several windows have the title (select one with App.WindowAt).

settings := app.Window("Settings")
settings.Click(telatest.ByRole("button", "Close"))
func (a *App) WindowAt(i int) *App

WindowAt returns the open window at index i in the order the windows opened (0 is the main window), waiting until there are that many.

func (a *App) WindowMenus() int

WindowMenus returns how many secondary presses in a caption region opened the window menu of the fake window manager.

func (a *App) WindowSize() (width, height float32)

WindowSize returns the size of the window’s content area in logical pixels, as the app was told (a maximized window has the fake screen’s size, see Screen).

func (a *App) WindowState() tela.WindowState

WindowState returns the window’s state as the app sees it (tela.WindowHandle.State): maximized, fullscreen and minimized as the fake window manager reported them, whether the window is the active one, whether the app hid it, and whether the app draws its title bar.

func (a *App) Windows() []*App

Windows returns the app’s open windows in the order they opened, the main window first: an App scoped to each, with the same queries, actions, expectations and goldens as the one Mount returns. A window that is hidden (the app’s Hidden, a hidden Window) or minimized is still open and listed. The app is settled first, so a window a handler just asked for is listed; use App.WaitForWindow or App.ExpectWindowCount for one that opens later (a goroutine, a timer). Like Playwright’s context.pages().

for _, w := range app.Windows() {
t.Logf("%q is %v", w.Title(), w.WindowState())
}
type ClickOption func(*clickConfig)

ClickOption configures App.Click and App.ClickAt.

func Delay(d time.Duration) ClickOption

Delay holds the button down for d of app time between the press and the release, like Playwright’s click delay. It is how a test makes a long press of the mouse (the arena recognizes one after 500 ms): the handler runs while the button is held and the release then ends the press without a click. For a touch long press, which opens a context menu, use App.LongPress.

func Right() ClickOption

Right makes App.Click and App.ClickAt press and release the secondary (right) mouse button, like Playwright’s click({button: ‘right’}): real pointer events with the secondary button, so the element’s OnContextMenu runs once the button is released (a kit.ContextMenu opens) and its OnClick does not. The element must be as actionable as for a primary click: visible, enabled, in view and not covered. It combines with Delay.

app.Click(telatest.ByTestID("area"), telatest.Right())
app.ClickAt(120, 80, telatest.Right())
type DesktopService uint8

DesktopService is a group of desktop services, for App.Unsupported.

const (
// FileDialogs: desktop.OpenFile, OpenFiles, SaveFile, PickFolder.
FileDialogs DesktopService = iota + 1
// MessageBoxes: the native message box of desktop.ShowMessage; without
// it the box is drawn in a window, as on Linux.
MessageBoxes
// Notifications: desktop.Notify.
Notifications
// TrayIcons: desktop.Tray.
TrayIcons
// OpenURLs: tela.OpenURL and tela.OpenPath.
OpenURLs
)
type DragOption func(*dragConfig)

DragOption configures App.Drag, App.DragBy and App.DragFromTo.

func Steps(n int) DragOption

Steps sets how many pointer moves the drag is made of (default 12): the pointer moves in equal steps between the start and the end, and each move runs a frame, as a window would.

type FileDialog struct {
Kind FileDialogKind
// Title, Directory and DefaultName are the options the app gave
// (desktop.Title, Directory, DefaultName).
Title, Directory, DefaultName string
// Filters are the desktop.Filter entries.
Filters []desktop.FileFilter
}

FileDialog is a file dialog the app opened and the test has not answered.

type FileDialogKind string

FileDialogKind is the kind of a FileDialog.

const (
OpenFileDialog FileDialogKind = "OpenFile" // desktop.OpenFile
OpenFilesDialog FileDialogKind = "OpenFiles" // desktop.OpenFiles (several files)
SaveFileDialog FileDialogKind = "SaveFile" // desktop.SaveFile
PickFolderDialog FileDialogKind = "PickFolder" // desktop.PickFolder
)
type FilterOption func(*Locator)

FilterOption narrows a locator in Locator.Filter.

func HasClass(classes ...string) FilterOption

HasClass keeps the elements that have all the given Tailwind class names. Like ByClass it ties the test to the styling.

func HasText(text string) FilterOption

HasText keeps the elements whose text content contains text (a case-insensitive substring, whitespace normalized), like Playwright’s filter({hasText}).

type GoldenOption struct {
// contains filtered or unexported fields
}

GoldenOption configures App.Golden.

func Tolerance(maxDelta uint8, maxPixels int) GoldenOption

Tolerance lets a golden comparison accept small differences: pixels whose channels differ by at most maxDelta (0 to 255, on premultiplied values) count as equal, and up to maxPixels pixels may differ by more. Without it a golden must match bit by bit: Tela’s pixels are the same on every platform (ADR-027), so only a deliberate change should alter one.

type HitKind = platform.HitKind

HitKind is what a region of a window’s custom title bar is to the OS.

HitKind is an alias of platform.HitKind, declared in an internal package of the module. pkg.go.dev shows only the alias; the exported fields and methods of the aliased type, listed here, are part of the API:

type HitKind uint8
const (
// HitClient is ordinary content. It is what lies outside every
// region, and a HitClient region punches a hole into the regions
// listed before it (a search field or a tab strip inside a caption
// area).
HitClient HitKind = iota
// HitCaption is a drag area of the title bar: the OS handles presses
// in it (move, double-click, window menu).
HitCaption
// HitMinimize, HitMaximize and HitClose are the window buttons drawn
// by the app. Pointer events over them reach the app as usual (its
// buttons perform the action); the region tells the OS what they are
// (Windows 11 snap layouts open from the maximize button).
HitMinimize
HitMaximize
HitClose
)
func (k HitKind) String() string
type HitRegion struct {
Kind HitKind
Rect tela.Rect
}

HitRegion is one rectangle of a window’s custom title bar, in logical pixels from the window’s top-left corner.

func (r HitRegion) String() string
type Locator struct {
// contains filtered or unexported fields
}

Locator describes the element an action or expectation works on, like a Playwright locator: it is resolved against the live semantics tree each time it is used, so it keeps working when the app rebuilds. Make one with ByRole, ByText, ByClass or ByTestID and narrow it with Locator.Filter and Locator.Nth.

Hidden elements (display: none, visibility other than visible, AriaHidden, and their descendants) are never matched. An action needs the locator to match exactly one element (strictness, as in Playwright) and fails with the aria snapshot and the closest names when it matches none or several.

func ByClass(classes ...string) Locator

ByClass locates the elements that have all the given Tailwind class names. It is an escape hatch for elements without a role or text, and ties the test to the styling: prefer ByRole and ByText.

telatest.ByClass("rounded-full", "shadow-sm")

Example

fmt.Println(telatest.ByClass("rounded-full", "shadow-sm"))
// Output: getByClass("rounded-full shadow-sm")
func ByRole(role, name string) Locator

ByRole locates the elements with an ARIA role (explicit with h.Role or implicit: button, heading, link, checkbox…) and, when name is not empty, exactly this accessible name (whitespace normalized, case sensitive). Elements without a role of their own (Div, Span) are never matched by role, as in Playwright. An empty name matches any name.

telatest.ByRole("button", "Increment")
telatest.ByRole("heading", "")

Example

fmt.Println(telatest.ByRole("button", "Increment"))
fmt.Println(telatest.ByRole("heading", ""))
// Output:
// getByRole("button", name="Increment")
// getByRole("heading")
func ByTestID(id string) Locator

ByTestID locates the element that carries the h.TestID with this exact id, like Playwright’s getByTestId. It is the way to find an element a user cannot name (a card, a chart, a drop zone); the id is not part of the semantics tree or the aria snapshot, so it is never visible to assistive technologies. Hidden elements are not matched, like with the other locators.

telatest.ByTestID("sidebar")

Example

card := func() tela.Node {
return Div(TestID("card"), Class("size-16 bg-slate-200"))
}
app := telatest.MountFunc(t, card, telatest.Size(100, 100))
fmt.Println(telatest.ByTestID("card"))
fmt.Println(app.Bounds(telatest.ByTestID("card")))
fmt.Println(app.AriaSnapshot() == "") // the id is not in the snapshot
// Output:
// getByTestId("card")
// {0 0 64 64}
// true
func ByText(text string, opts ...TextOption) Locator

ByText locates the elements that contain text, like Playwright’s getByText: it matches the text content of an element (whitespace normalized, hidden descendants left out, block boundaries counting as spaces) and picks the deepest elements that match, so the button that holds the text rather than the page that holds the button. The match is a case-insensitive substring unless Exact is given.

telatest.ByText("Count: 1")
telatest.ByText("Save", telatest.Exact())

Example

fmt.Println(telatest.ByText("Count"))
fmt.Println(telatest.ByText("Save", telatest.Exact()))
// Output:
// getByText("Count")
// getByText("Save", exact)
func (l Locator) Filter(opts ...FilterOption) Locator

Filter returns a locator that matches only the elements of l that satisfy the options.

telatest.ByRole("button", "").Filter(telatest.HasText("System"))

Example

rows := telatest.ByRole("button", "").Filter(telatest.HasText("System"))
fmt.Println(rows)
// Output: getByRole("button").filter(hasText="System")
func (l Locator) Nth(i int) Locator

Nth returns a locator for the i-th match of l (0 is the first; a negative i counts from the end, so -1 is the last). It turns a locator that matches several elements into one that matches one.

Example

fmt.Println(telatest.ByRole("listitem", "").Nth(0))
fmt.Println(telatest.ByRole("listitem", "").Nth(-1)) // the last one
// Output:
// getByRole("listitem").nth(0)
// getByRole("listitem").nth(-1)
func (l Locator) String() string

String describes the locator the way Playwright does.

type MessageBox struct {
Kind desktop.MessageKind
Title string
Text string
Buttons desktop.Buttons
// Drawn: Tela draws the box in a window (the desktop has no native
// message box, as on Linux; see [App.Unsupported]).
Drawn bool
}

MessageBox is a message box the app shows (desktop.ShowMessage) and the test has not answered.

type Notification struct {
Title, Body string
// Actions are the labels of its buttons.
Actions []string
// Closed: the app closed it, or the user clicked or dismissed it.
Closed bool
}

Notification is a notification the app showed (desktop.Notify).

type Option func(*config)

Option configures Mount and the headless entry points Render, AriaSnapshot and LayoutJSON.

func AllowDev(substrings ...string) Option

AllowDev lists dev errors that do not fail a test: those containing any of the substrings, which are logged instead. Dev warnings never fail a test, and AllowDev does nothing under NoDev.

func AlwaysOnTop() Option

AlwaysOnTop keeps the main window above the others (tela.App.AlwaysOnTop).

func AppID(id string) Option

AppID sets the application identity the window reports to the desktop (tela.App.AppID).

func Backdrop(b tela.Backdrop) Option

Backdrop sets the system backdrop of the main window (tela.App.Backdrop). The fake window manager only records it: App.Backdrop reads it back.

func CheckDamage() Option

CheckDamage makes the app compare its window with a full rendering after every frame (when nothing is left to draw), and fail the test on the first pixel that differs: frames are rasterized incrementally (only what changed, scrolled content shifted), and must give the pixels a full rasterization gives. It makes every frame cost a full rendering; it is for tests of rendering, and TELA_CHECK_DAMAGE=1 turns it on everywhere.

func ClassAliases(aliases map[string]string) Option

ClassAliases sets the app’s class aliases, like tela.App.ClassAliases: class names that expand to class strings where they appear.

func ClassicScrollbars() Option

ClassicScrollbars draws always-visible classic scrollbars that take space (tela.App.ClassicScrollbars).

func ColorScheme(s tela.ColorScheme) Option

ColorScheme sets the app’s color scheme, like tela.App.ColorScheme: tela.Light or tela.Dark override the system’s preference (Dark sets that one), tela.System follows it. tela.SetColorScheme changes it while the app runs.

Example

swatch := Div(Class("size-8 bg-white dark:bg-black"))
light, _ := telatest.Render(swatch, telatest.Size(8, 8), telatest.Dark(), telatest.ColorScheme(tela.Light))
dark, _ := telatest.Render(swatch, telatest.Size(8, 8), telatest.ColorScheme(tela.Dark))
fmt.Println(light.RGBAAt(4, 4), dark.RGBAAt(4, 4))
// Output: {255 255 255 255} {0 0 0 255}
func Dark() Option

Dark makes the system prefer the dark color scheme, so the dark: variants apply unless the app sets another scheme (ColorScheme, tela.App.ColorScheme, tela.SetColorScheme).

func Debug(flags ...string) Option

Debug turns debug overlays on: any of “layout”, “paint-flash”, “layers”, “fps” and “semantics” (or “all”), the values TELA_DEBUG takes. The overlays are drawn over the window, so App.Screenshot and App.Golden show them; the headless entry points (Render) draw them too. TELA_DEBUG itself is ignored, so goldens never depend on the environment. An unknown name fails the test with the valid ones.

A paint flash fades over 300 ms of app time, and the fps meter redraws on every frame; neither keeps an action waiting (they are not work the app must finish), so look at them with App.Advance. See DebugClock for the frame times the meter shows.

func DebugClock(now func() time.Duration) Option

DebugClock replaces the clock the fps overlay times frames with: a frame took the difference between two readings of now, taken before and after it. The default reads 1 ms longer at every call, so every frame takes 1 ms and goldens do not depend on the machine; tests of the meter pass a scripted clock.

func Dev() Option

Dev turns dev mode on. It is the default since dev mode became the default (see NoDev), so Dev changes nothing; it is kept for the tests written before, and undoes a NoDev given earlier in the options.

func FixedSize() Option

FixedSize keeps the user from resizing the main window (tela.App.FixedSize).

func Frameless() Option

Frameless gives the main window no OS title bar (tela.App.Frameless): its tela.WindowState reports ClientSideDecorations, and the content draws its own title bar with h.WindowDrag and h.WindowControl.

func FromApp(app tela.App) Option

FromApp applies the settings of a tela.App that shape what is drawn, so a test, tela snapshot and the window see the same app: the size when both Width and Height are set, RootClass, ColorScheme, Theme, ClassAliases, ClassicScrollbars, InstantScroll and Dev. It sets only what the app sets, so it never undoes an earlier option, and the options after it override it:

var App = tela.App{Title: "Notes", Width: 900, Height: 600, RootClass: "bg-background", Root: Notes()}
app := telatest.Mount(t, Notes(), telatest.FromApp(App))
app := telatest.Mount(t, Notes{}, telatest.FromApp(App), telatest.Size(400, 300))

Title, AppID, Root, DevState and the fields of the OS window (size limits, FixedSize, Frameless, AlwaysOnTop, Hidden, Icon, Placement, OnClose) say nothing about how the content looks, so FromApp ignores them: tools that only render the app (tela snapshot, tela mcp) want it visible and quit-able whatever the app does with its window. MountApp mounts Root and applies those fields too, as the options Title, MinSize, Frameless, StartHidden, Placement, OnClose and the others do by hand. The scale is not an App setting (it comes from the display): use Scale.

Example

appSettings := tela.App{Width: 400, Height: 300, RootClass: "bg-slate-100"}
// Mount a struct component (or a tela.Preview) in the app's window.
app := telatest.Mount(t, tela.C(Counter), telatest.FromApp(appSettings), telatest.Scale(2))
fmt.Println(app.Screenshot().Bounds().Size())
// Output: (800,600)
func Icon(images ...image.Image) Option

Icon sets the icon of the main window (tela.App.Icon): the same picture at several sizes.

func InstantScroll() Option

InstantScroll makes user scrolling jump, without animation or flings (tela.App.InstantScroll).

func MaxSize(width, height float32) Option

MaxSize limits the size of the main window from above; see MinSize.

func MinSize(width, height float32) Option

MinSize and MaxSize limit the size the user can give the main window (tela.App.MinWidth and MinHeight, MaxWidth and MaxHeight); zero is no limit on that side. The fake window manager clamps App.Resize, maximizing and fullscreen to them.

func NoDev() Option

NoDev turns dev mode off. Apps and the headless entry points run in dev mode by default, as a development build of an app does: unknown classes, roles, input types and easings, hooks called in another order or outside their component, and the dev warnings (lists without keys, nil dependencies, a Value read while changes are queued, a Label without a control…) are reported. With Mount, a dev error (an unknown class, an image that cannot be loaded) fails the test at the action that caused it unless AllowDev lists it, a dev panic fails it as any panic does, and a warning is a test log line; with Render and the other entry points, errors and warnings go to standard error.

Use NoDev to test what a release build does. The checks of the node constructors (h.If with a struct component, Role, Type, Dir…) follow the app’s dev mode when they run while a component builds, as in a function given to MountFunc; a node built outside any build (the root passed to Mount in the test’s body) follows the process default, which is on in a test binary. TELA_DEV=1 or TELA_DEV=0 in the environment overrides both NoDev and the default.

Example

// A typo in a class: in dev mode (the default) it fails the test with
// the closest class, "rounded-2xl".
card := func() tela.Node { return Div(Class("rounded-xxl p-4"), Text("Card")) }
// NoDev tests what a release build does: the class is ignored and the
// test goes on.
app := telatest.Mount(t, tela.C(card), telatest.Size(200, 100), telatest.NoDev())
app.ExpectText("Card")
fmt.Println("ok")
// Output: ok
func NoServerDecorations() Option

NoServerDecorations makes the fake window manager draw no title bars, as GNOME on Wayland does: every window, framed or not, reports ClientSideDecorations, so a test can check that the app draws its own bar (kit.TitleBar) there.

func NoSettle() Option

NoSettle makes the app keep its animations where they are for App.Screenshot and App.Golden, so a test can capture the frames of a transition, a tela.UseSpring or the enter and exit of an h.Presence:

app := telatest.Mount(t, Dialog(), telatest.NoSettle())
app.Click(telatest.ByRole("button", "Open"))
app.Advance(100 * time.Millisecond)
app.Golden("dialog_100ms.png") // 100 ms into the entrance

With it, Mount, the events of an action, Screenshot and Golden and the queries App.AriaSnapshot, App.Count, App.Bounds, App.Stats and App.Cursor handle the work due (state updates, the frames they ask for) but do not run the clock until running transitions and animations end: the clock moves with App.Advance. The expectations poll as ever, but their first try handles only the work due, and the clock moves a frame at a time only between failed tries. What keeps its full auto-wait is the wait before an action: before acting, an action waits for the app to settle (so the animations an earlier step started end first).

Example

// A box that fades in over 200 ms when it is shown.
page := func() tela.Node {
shown, setShown := tela.UseState(false)
return Div(
Button(OnClick(func(tela.MouseEvent) { setShown.Set(true) }), Text("Show")),
If(shown, Div(Class("size-10 bg-black transition-opacity duration-200 ease-linear starting:opacity-0"))))
}
app := telatest.Mount(t, tela.C(page), telatest.Size(100, 100), telatest.NoSettle())
app.Click(telatest.ByRole("button", "Show"))
app.Advance(100 * time.Millisecond)
// Halfway: black at 50% over white. app.Golden("fade_100ms.png") would
// capture the same frame.
fmt.Println(app.Screenshot().RGBAAt(5, 40))
app.Advance(100 * time.Millisecond)
fmt.Println(app.Screenshot().RGBAAt(5, 40))
// Output:
// {128 128 128 255}
// {0 0 0 255}
func OnClose(f func(tela.WindowHandle)) Option

OnClose sets what closing the main window does (tela.App.OnClose): f runs when the user asks to close it (App.RequestClose, a WindowClose control, WindowHandle.Close). Without it the app quits.

func Placement(p tela.WindowPlacement) Option

Placement opens the main window where a previous run left it (tela.App.Placement): a tela.WindowPlacement saved from tela.WindowHandle.Placement or App.Placement. Its size, when set, wins over Size.

func ReducedMotion() Option

ReducedMotion makes the system ask for reduced motion: the motion-reduce: variants apply, transitions take no time and scrolling jumps.

func RootClass(classes string) Option

RootClass sets the classes of the root box, which fills the window (tela.App.RootClass): a background, a text style, overflow-hidden.

func Scale(scale float64) Option

Scale sets the display scale: device pixels per logical pixel (default 1). Screenshots and goldens are (width*scale) x (height*scale) pixels.

func Screen(width, height float32) Option

Screen sets the logical size of the fake screen: what maximized and fullscreen windows grow to (default 1920 by 1080).

func Size(width, height float32) Option

Size sets the logical size of the window in pixels (default 800x600).

func StartHidden() Option

StartHidden opens the main window hidden (tela.App.Hidden): an app that starts in the tray. The fake window manager gives a hidden window no frames, so it draws nothing (no screenshot, no goldens, no actions that need a layout) until the app shows it; its components still build and its effects run (its tray icon shows, ADR-185).

func Theme(t tela.Theme) Option

Theme sets the app’s theme, like tela.App.Theme: the color tokens (bg-primary, text-muted-foreground), the base radius and the fonts. Without it the app has tela.DefaultTheme. tela.SetTheme changes it while the app runs.

func Timeout(d time.Duration) Option

Timeout sets how long actions and expectations wait for their condition before they fail the test (default DefaultTimeout). It is wall-clock time, as in Playwright, while the app’s clock moves one frame at a time during the wait. It also bounds, in app time (up to DefaultTimeout), how long a single action lets the app run before it stops waiting for it to go quiet (an animation that never ends is logged, not waited for); a finite animation runs to its end however slow the machine is.

func Title(title string) Option

Title sets the title of the main window (tela.App.Title), which App.Title and App.Window read. The default is “telatest”.

func TitleBar(colors tela.TitleBarColors) Option

TitleBar sets the title bar colors of the main window (tela.App.TitleBar). The fake window manager only records them: App.TitleBar reads them back.

type State struct {
// contains filtered or unexported fields
}

State is one condition on an element that App.Expect checks: a toggle such as Checked, a flag such as Disabled, its HasName and HasValue, or a widget property the aria snapshot does not print: HasPopup, ActiveDescendant and Controls (which compare the element an ID reference points to with the one a Locator finds). They read the element’s semantics node, so they test what assistive technologies are told, without matching strings of the aria snapshot.

func ActiveDescendant(loc Locator) State

ActiveDescendant expects the element’s aria-activedescendant to point to the element that loc locates (one element, strictness): the option a combobox or a list box keeps its virtual focus on while the keyboard focus stays on the input (h.AriaActiveDescendant sets it). Use NoActiveDescendant for none. The aria snapshot does not print it.

app.Expect(telatest.ByRole("combobox", "Project"),
telatest.ActiveDescendant(telatest.ByRole("option", "users-db")))
func Checked(checked bool) State

Checked expects the element to have aria-checked equal to checked (a checkbox, radio, switch or option; h.AriaChecked sets it). An element without the state, or with the mixed one, does not satisfy it.

func Controls(loc Locator) State

Controls expects the element’s aria-controls to point to the element that loc locates (one element, strictness): the list box of a combobox or the panel of a tab (h.AriaControls sets it). The aria snapshot does not print it.

app.Expect(telatest.ByRole("combobox", "Project"), telatest.Controls(telatest.ByRole("listbox", "Project")))
func Current(current string) State

Current expects the element’s aria-current to be current: “page”, “step”, “location”, “date”, “time” or “true”, or “false”, which is also what an element without h.AriaCurrent has (the ARIA default). The aria snapshot does not print it (Playwright’s does not either).

app.Click(telatest.ByRole("link", "Docs"))
app.Expect(telatest.ByRole("link", "Docs"), telatest.Current("page"))
func Disabled(disabled bool) State

Disabled expects the element to be disabled (true) or enabled (false): disabled by h.Disabled on it or an ancestor, or by h.AriaDisabled.

func Expanded(expanded bool) State

Expanded expects the element to have aria-expanded equal to expanded (what a button, link or combobox controls; h.AriaExpanded sets it).

func Focused(focused bool) State

Focused expects the element to have the keyboard focus (true) or not (false).

func HasName(name string) State

HasName expects the element’s accessible name to be exactly name (white space normalized, case sensitive): what ByRole matches by.

func HasPopup(kind string) State

HasPopup expects the element’s aria-haspopup to be kind: “menu”, “listbox”, “tree”, “grid” or “dialog” (h.AriaHasPopup sets it), or “” for an element that opens no popup. As in ARIA, “true” is “menu” and “false” is “”. The aria snapshot does not print it (Playwright’s does not either).

app.Expect(telatest.ByRole("button", "Show suggestions"), telatest.HasPopup("listbox"))
func HasValue(value string) State

HasValue expects the element’s value to be exactly value: the text of a text field, or the value text of a range widget (the h.AriaValueText or h.AriaValueNow of a slider or progress bar, as in the aria snapshot).

func Invalid(invalid bool) State

Invalid expects the element’s value to be invalid (true) or valid (false) for assistive technologies: h.AriaInvalid when it is set, else a text field’s own validity (an email or url Input whose value does not parse; a ReadOnly or disabled field is never invalid by its value), as Chromium tells them.

app.Fill(telatest.ByRole("textbox", "Email"), "ada@")
app.Expect(telatest.ByRole("textbox", "Email"), telatest.Invalid(true))
func NoActiveDescendant() State

NoActiveDescendant expects the element to have no active descendant (no aria-activedescendant, or one whose element is not in the tree).

func Pressed(pressed bool) State

Pressed expects the element to have aria-pressed equal to pressed (a toggle button; h.AriaPressed sets it).

func Selected(selected bool) State

Selected expects the element to have aria-selected equal to selected (a tab, option, row or cell; h.AriaSelected sets it).

func Sort(sort string) State

Sort expects the element’s aria-sort to be sort: “ascending”, “descending”, “other” or “none”, which is also what a header without h.AriaSort has (the ARIA default). The aria snapshot does not print it (Playwright’s does not either).

app.Click(telatest.ByRole("columnheader", "Name"))
app.Expect(telatest.ByRole("columnheader", "Name"), telatest.Sort("descending"))
type Stats = core.Stats

Stats counts the work of the app’s tree since it was mounted: frames, component builds and skipped builds, style, layout and paint passes. A test uses the difference between two readings to assert how narrow an update was.

Stats is an alias of core.Stats, declared in an internal package of the module. pkg.go.dev shows only the alias; the exported fields and methods of the aliased type, listed here, are part of the API:

type Stats struct {
Frames int // Frame calls that ran the pipeline
Builds int // component builds
Skips int // component builds skipped (equal props, or the same node)
Mounts int // Elements mounted
Unmounts int // Elements unmounted
Styles int // style computations
Structures int // box structure passes
Shapes int // paragraphs shaped (text.NewLayout)
Breaks int // paragraph line breakings (text.Layout.BreakLines)
Layouts int // layout passes (layout.Engine.Compute)
Paints int // paint passes
}
type TextOption func(*Locator)

TextOption configures ByText.

func Exact() TextOption

Exact makes ByText match the whole text, case sensitive, instead of a case-insensitive substring.

type TrayIcon struct {
Tooltip string
Menu []TrayItem
// Updates counts the changes of the icon since it was added.
Updates int
}

TrayIcon is a tray icon the app shows (a mounted desktop.Tray).

type TrayItem struct {
Label string
Separator, Checkbox, Checked bool
Disabled bool
Submenu []TrayItem
}

TrayItem is an item of a tray icon’s menu.

type WindowCond struct {
// contains filtered or unexported fields
}

WindowCond is a condition on a window’s state for App.ExpectWindowState.

func Active(on bool) WindowCond

Active is met when the window is the active one, the one with the keyboard focus (WindowState.Focused), or is not.

func ClientDecorations(on bool) WindowCond

ClientDecorations is met when the app draws the window’s title bar (WindowState.ClientSideDecorations: a Frameless window), or does not.

func Fullscreen(on bool) WindowCond

Fullscreen is met when the window is (or is not) fullscreen.

func Hidden(on bool) WindowCond

Hidden is met when the app has hidden the window (tela.Window.Hidden, WindowHandle.Hide), or has not.

func Maximized(on bool) WindowCond

Maximized is met when the window is (or is not) maximized.

func Minimized(on bool) WindowCond

Minimized is met when the window is (or is not) minimized.