mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
test(runner): element-valued extractors reach the trace
This commit is contained in:
1 parent
91ca574d67
commit
ff4666c126
1 file changed
+135
@@ -0,0 +1,135 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/priyanshujain/sanderling/internal/trace"
|
||||
)
|
||||
|
||||
// elementExtractorSpec records accessibility elements directly, the shape an
|
||||
// author reaches for when the question is "what was on screen at this step":
|
||||
// one element and the list a generator would have been offered.
|
||||
const elementExtractorSpec = `
|
||||
import { actions, extract } from "@sanderling/spec";
|
||||
extract("field", state => state.ax.find({ testTag: "LoginEmail" }));
|
||||
extract("rows", state => state.ax.findAll({ testTag: "Row" }));
|
||||
globalThis.properties = {};
|
||||
globalThis.actions = actions(() => []);
|
||||
`
|
||||
|
||||
const elementExtractorHierarchy = `{
|
||||
"attributes": {"class": "android.widget.LinearLayout", "bounds": "[0,0,1080,2340]"},
|
||||
"children": [
|
||||
{"attributes": {"resource-id": "LoginEmail", "class": "android.widget.EditText",
|
||||
"text": "[email protected]", "bounds": "[10,20,200,60]"}, "children": []},
|
||||
{"attributes": {"resource-id": "Row", "class": "android.widget.TextView",
|
||||
"text": "first", "bounds": "[0,100,1080,200]"}, "children": []},
|
||||
{"attributes": {"resource-id": "Row", "class": "android.widget.TextView",
|
||||
"text": "second", "bounds": "[0,200,1080,300]"}, "children": []}
|
||||
]
|
||||
}`
|
||||
|
||||
// TestRunner_TraceRecordsElementValuedExtractors pins that an extractor holding
|
||||
// an accessibility element reaches the trace. Elements carry host functions
|
||||
// (find/findAll), which json.Marshal refuses; the encoder used to answer nil
|
||||
// and the diff then emitted no entry, so the run finished clean with the
|
||||
// extractor missing from every step and no error anywhere.
|
||||
func TestRunner_TraceRecordsElementValuedExtractors(t *testing.T) {
|
||||
state := newHarnessWithSpec(t, elementExtractorSpec)
|
||||
state.mock.HierarchyJSON = elementExtractorHierarchy
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if _, err := Run(ctx, Options{
|
||||
Duration: 100 * time.Millisecond,
|
||||
IdleTimeout: 20 * time.Millisecond,
|
||||
Driver: state.mock,
|
||||
Verifier: state.verifier,
|
||||
TraceWriter: state.writer,
|
||||
}); err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
field, rows := elementChangesFromTrace(t, state.directory)
|
||||
if field == nil {
|
||||
t.Fatal("no extractor change for the element-valued extractor reached the trace")
|
||||
}
|
||||
if rows == nil {
|
||||
t.Fatal("no extractor change for the element-list extractor reached the trace")
|
||||
}
|
||||
|
||||
var element map[string]any
|
||||
if err := json.Unmarshal(field, &element); err != nil {
|
||||
t.Fatalf("field value is not a JSON object: %v (%s)", err, field)
|
||||
}
|
||||
if got := element["id"]; got != "LoginEmail" {
|
||||
t.Errorf("field.id = %v, want LoginEmail", got)
|
||||
}
|
||||
if got := element["text"]; got != "[email protected]" {
|
||||
t.Errorf("field.text = %v, want [email protected]", got)
|
||||
}
|
||||
if got := element["class"]; got != "android.widget.EditText" {
|
||||
t.Errorf("field.class = %v, want android.widget.EditText", got)
|
||||
}
|
||||
bounds, ok := element["bounds"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("field.bounds missing or not an object: %v", element["bounds"])
|
||||
}
|
||||
if bounds["left"] != float64(10) || bounds["top"] != float64(20) ||
|
||||
bounds["right"] != float64(200) || bounds["bottom"] != float64(60) {
|
||||
t.Errorf("field.bounds = %v, want left/top/right/bottom 10/20/200/60", bounds)
|
||||
}
|
||||
for _, key := range []string{"find", "findAll"} {
|
||||
if _, present := element[key]; present {
|
||||
t.Errorf("field carries the host function %q into the trace", key)
|
||||
}
|
||||
}
|
||||
|
||||
var list []map[string]any
|
||||
if err := json.Unmarshal(rows, &list); err != nil {
|
||||
t.Fatalf("rows value is not a JSON array: %v (%s)", err, rows)
|
||||
}
|
||||
if len(list) != 2 {
|
||||
t.Fatalf("rows recorded %d elements, want 2", len(list))
|
||||
}
|
||||
if list[0]["text"] != "first" || list[1]["text"] != "second" {
|
||||
t.Errorf("rows recorded %v, want the two Row elements in tree order", list)
|
||||
}
|
||||
}
|
||||
|
||||
// elementChangesFromTrace returns the first recorded value of each extractor.
|
||||
func elementChangesFromTrace(t *testing.T, directory string) (field, rows json.RawMessage) {
|
||||
t.Helper()
|
||||
file, err := os.Open(filepath.Join(directory, "trace.jsonl"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
|
||||
for scanner.Scan() {
|
||||
var line struct {
|
||||
ExtractorChanges map[string]trace.ExtractorChange `json:"extractor_changes"`
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &line); err != nil {
|
||||
t.Fatalf("trace line decode: %v", err)
|
||||
}
|
||||
if change, ok := line.ExtractorChanges["field"]; ok && field == nil {
|
||||
field = change.Curr
|
||||
}
|
||||
if change, ok := line.ExtractorChanges["rows"]; ok && rows == nil {
|
||||
rows = change.Curr
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
t.Fatalf("scan trace: %v", err)
|
||||
}
|
||||
return field, rows
|
||||
}
|
||||
Reference in new issue
Block a user