mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
CDP exposes no per-page CPU, so chrome's Metrics filled CPUPercent with a zero, which is the same answer an idle app gives. Making the field optional is the same fix as the iOS reads: absence is the only honest way to say nothing looked.
217 lines
9.7 KiB
Go
217 lines
9.7 KiB
Go
// Package driver defines the platform-agnostic device automation interface and shared types.
|
|
package driver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// ErrGestureUndelivered reports a coordinate gesture that reached no element at
|
|
// all, so the app cannot have responded to it. It is not a device fault and
|
|
// says nothing about the device's health: the runner records it on the step
|
|
// rather than counting it toward the apply-failure streak, which is what makes
|
|
// a gesture that did nothing distinguishable from one the app ignored.
|
|
var ErrGestureUndelivered = errors.New("gesture reached no element")
|
|
|
|
// ErrSelectorMatchedNothing reports an action dispatched by selector whose
|
|
// selector named nothing on the current screen. It is a resolution failure, not
|
|
// a delivery one: no point was ever computed, so it stays separate from
|
|
// ErrGestureUndelivered and the runner records it as an unresolved selector.
|
|
var ErrSelectorMatchedNothing = errors.New("selector matched no element")
|
|
|
|
// ErrNotSupported reports a contract method the driver has no way to answer on
|
|
// its platform. It is neither a device fault nor an observation: a caller that
|
|
// reads the zero value as one records a reading it never took, which is what
|
|
// lets a property over a channel the driver never opened hold vacuously with
|
|
// nothing in the run's output saying so. Callers check it with errors.Is and
|
|
// record that the read did not happen.
|
|
var ErrNotSupported = errors.New("driver: capability not supported")
|
|
|
|
// DeviceDriver abstracts the platform-specific UI automation backend. v0.1
|
|
// surface matches proto/driverpb/driver.proto. The sidecar implementation
|
|
// lives under driver/sidecar; the web implementation under driver/chrome;
|
|
// tests use driver/mock.
|
|
type DeviceDriver interface {
|
|
Launch(ctx context.Context, bundleID string, clearState bool, env map[string]string) error
|
|
Terminate(ctx context.Context) error
|
|
|
|
Tap(ctx context.Context, x, y int) error
|
|
TapSelector(ctx context.Context, selector string) error
|
|
DoubleTap(ctx context.Context, x, y int) error
|
|
DoubleTapSelector(ctx context.Context, selector string) error
|
|
InputText(ctx context.Context, text string) error
|
|
// EraseText deletes characterCount characters from the focused field.
|
|
// The runner calls it before InputText so the verb replaces existing
|
|
// content instead of appending to it.
|
|
EraseText(ctx context.Context, characterCount int) error
|
|
Swipe(ctx context.Context, fromX, fromY, toX, toY int, duration time.Duration) error
|
|
PressKey(ctx context.Context, key string) error
|
|
LongPress(ctx context.Context, x, y int) error
|
|
|
|
Hierarchy(ctx context.Context) (string, error)
|
|
Screenshot(ctx context.Context) (Image, error)
|
|
// Snapshot returns the hierarchy and screenshot as one paired capture,
|
|
// the closest a driver can put them and the reason to prefer it over
|
|
// separate Hierarchy and Screenshot reads. It is not atomic: no driver
|
|
// freezes the frame while the two reads run.
|
|
Snapshot(ctx context.Context) (string, Image, error)
|
|
// RecentLogs returns log entries at or after `since`, filtered to
|
|
// `minLevel` or above. Drivers disagree on an empty minLevel (sidecar
|
|
// defaults to "E", Chrome returns every level), so the runner always
|
|
// passes one. A driver with no log source returns ErrNotSupported, not
|
|
// an empty slice: only one of the two means the app logged nothing.
|
|
RecentLogs(ctx context.Context, since time.Time, minLevel string) ([]LogEntry, error)
|
|
|
|
WaitForIdle(ctx context.Context, duration time.Duration) error
|
|
// Health reports whether the backend is attached and serving. A driver
|
|
// that runs no readiness check returns ErrNotSupported; Ready is a
|
|
// verdict, so reporting it true without one tells the caller a check
|
|
// passed that never ran.
|
|
Health(ctx context.Context) (Health, error)
|
|
// Metrics samples the app's memory, plus CPU where the platform exposes
|
|
// it (Chrome does not). CPUPercent is percent of a single core, so
|
|
// multi-core apps can exceed 100. HeapBytes and TotalMemoryBytes are the
|
|
// platform's nearest pair: RSS and virtual size on Android, used and
|
|
// total JS heap on Chrome. A driver that cannot sample returns
|
|
// ErrNotSupported rather than a zeroed sample.
|
|
Metrics(ctx context.Context, bundleID string) (Metrics, error)
|
|
}
|
|
|
|
// ForegroundChecker is the optional capability for reporting which app is
|
|
// currently in the foreground. The runner uses it to keep exploration scoped
|
|
// to the app under test: when an action backs out of (or otherwise leaves) the
|
|
// app, the runner relaunches it before acting again. Drivers that cannot
|
|
// determine the foreground app simply do not implement this interface.
|
|
type ForegroundChecker interface {
|
|
// ForegroundApp returns the bundle id / package of the app currently in
|
|
// the foreground. An empty string means "unknown" and the runner skips
|
|
// enforcement for that step rather than relaunching blindly.
|
|
ForegroundApp(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// Scroller is the optional capability for drivers whose scroll interaction is
|
|
// not a finger drag. On a touch device the two are the same gesture, so a
|
|
// driver that does not implement this gets its Scroll actions as a Swipe. A
|
|
// browser scrolls on wheel input instead, and treats a drag as a drag.
|
|
type Scroller interface {
|
|
// Scroll moves the content under (fromX, fromY) by the vector to the
|
|
// destination point, the same endpoints Swipe takes.
|
|
Scroll(
|
|
ctx context.Context,
|
|
fromX, fromY, toX, toY int,
|
|
duration time.Duration,
|
|
) error
|
|
}
|
|
|
|
// TextReplacer is the optional capability for drivers whose InputText already
|
|
// replaces the field's content instead of appending to it. The runner must
|
|
// skip its pre-erase for such drivers: the erase would be a redundant
|
|
// round-trip on every InputText.
|
|
type TextReplacer interface {
|
|
// ReplacesTextOnInput reports whether InputText replaces existing
|
|
// content, making the runner's pre-erase unnecessary.
|
|
ReplacesTextOnInput() bool
|
|
}
|
|
|
|
// FocusedWindowChecker is the optional capability for reporting which app owns
|
|
// the focused (on-screen) window. The startup gate uses it on top of
|
|
// ForegroundChecker, never instead: the resumed-activity signal flips to a
|
|
// freshly launched app before its first frame draws, so a gate on that alone
|
|
// can let the first observe read the previous app's screen.
|
|
type FocusedWindowChecker interface {
|
|
// FocusedWindowApp returns the package owning the focused window, or ""
|
|
// when no window is focused yet (e.g. mid-launch transition).
|
|
FocusedWindowApp(ctx context.Context) (string, error)
|
|
}
|
|
|
|
// LogEntry is one line of device log. Level is logcat's single-letter scale on
|
|
// every platform: "V", "D", "I", "W", "E", "F", ordered as written. The runner
|
|
// fetches at "E" and the default properties count entries whose level equals
|
|
// "E", so a driver that spells a level any other way empties the channel
|
|
// without failing anything: the entries never arrive and every property reading
|
|
// state.logs holds vacuously.
|
|
type LogEntry struct {
|
|
UnixMillis int64
|
|
Level string
|
|
Tag string
|
|
Message string
|
|
}
|
|
|
|
// ExceptionReporter is the optional capability for reporting the uncaught
|
|
// errors an app has captured so far. The runner feeds them to state.exceptions,
|
|
// which the default noUncaughtExceptions property reads. Drivers with no way to
|
|
// observe them simply do not implement it and the property stays vacuous there.
|
|
type ExceptionReporter interface {
|
|
Exceptions(ctx context.Context) ([]Exception, error)
|
|
}
|
|
|
|
// NavigationReporter is the optional capability for reporting the
|
|
// document-replacing navigations seen since the last call. A navigation
|
|
// restarts the app's own runtime, so a trace without them cannot separate an
|
|
// app that reloaded from a generator that repeated itself.
|
|
type NavigationReporter interface {
|
|
Navigations(ctx context.Context) ([]Navigation, error)
|
|
}
|
|
|
|
// Navigation is one document-replacing navigation: a reload, a form submit, a
|
|
// route change that swapped the document.
|
|
type Navigation struct {
|
|
URL string
|
|
UnixMillis int64
|
|
}
|
|
|
|
// Exception is one uncaught throwable the app captured.
|
|
type Exception struct {
|
|
Class string
|
|
Message string
|
|
StackTrace string
|
|
UnixMillis int64
|
|
}
|
|
|
|
type Image struct {
|
|
PNG []byte
|
|
Width int
|
|
Height int
|
|
}
|
|
|
|
type Health struct {
|
|
Ready bool
|
|
Version string
|
|
Platform string
|
|
}
|
|
|
|
type Metrics struct {
|
|
// CPUPercent is nil on a driver that samples memory but not CPU, which
|
|
// Chrome does: zero is a reading an idle app can produce, so a driver that
|
|
// never looked has to answer with nothing rather than with that.
|
|
CPUPercent *float64
|
|
HeapBytes int64
|
|
TotalMemoryBytes int64
|
|
}
|
|
|
|
// WebDriver is the optional capability surface exposed by the chrome driver
|
|
// for the V8-native tick path. The runner type-asserts on this interface;
|
|
// mobile drivers stay binary-compatible by simply not implementing it.
|
|
//
|
|
// Element references never cross V8/host. V8 serializes targets as {x, y}
|
|
// (or bounds) into the returned action JSON; the host dispatches via the
|
|
// normal DeviceDriver methods (Tap, InputText, etc.).
|
|
type WebDriver interface {
|
|
// InstallBundle injects the given JS source so it runs once per
|
|
// freshly-navigated document, plus immediately in the current page.
|
|
// The bundle is expected to register globals
|
|
// `__sanderlingExtractors__` and `__sanderlingNextAction__` on
|
|
// `window`.
|
|
InstallBundle(ctx context.Context, source []byte) error
|
|
// EvaluateExtractors invokes the extractor table installed by the
|
|
// bundle and returns each extractor's JSON-encoded current value
|
|
// keyed by its registration index.
|
|
EvaluateExtractors(ctx context.Context) (map[int]json.RawMessage, error)
|
|
// NextActionFromV8 invokes the action generator installed by the
|
|
// bundle and returns the resulting Action JSON for the host to
|
|
// dispatch. The shape mirrors verifier.Action's JSON form.
|
|
NextActionFromV8(ctx context.Context) (json.RawMessage, error)
|
|
}
|