feat(runner): V8 extractor overrides + V8 action source for WebDriver

When the driver implements WebDriver, the runner sources extractor values
from V8 (real DOM) and the next action from the V8-side action generator.
LTL property predicates still run host-side in goja.
This commit is contained in:
pj committed 2026-04-27 18:50:43 +07:00
1 parent a31d9869b1
commit 3aa6745f97
1 file changed
+76 -1
+76 -1
View File
@@ -90,11 +90,21 @@ func Run(ctx context.Context, options Options) (Summary, error) {
logs = collectLogs(ctx, options.Driver, logSince) logs = collectLogs(ctx, options.Driver, logSince)
return nil return nil
}) })
var v8Overrides map[int]json.RawMessage
if web, ok := options.Driver.(driver.WebDriver); ok { if web, ok := options.Driver.(driver.WebDriver); ok {
g.Go(func() error { g.Go(func() error {
htmlCaptured = captureHTML(ctx, web, options.TraceWriter, logger, si, false) htmlCaptured = captureHTML(ctx, web, options.TraceWriter, logger, si, false)
return nil return nil
}) })
g.Go(func() error {
overrides, err := web.EvaluateExtractors(ctx)
if err != nil {
logger.Warn("v8 extractor evaluation failed", "step", si, "err", err)
return nil
}
v8Overrides = overrides
return nil
})
} }
if pendingPostScreenshot { if pendingPostScreenshot {
postStep := pendingPostScreenshotStep postStep := pendingPostScreenshotStep
@@ -133,6 +143,9 @@ func Run(ctx context.Context, options Options) (Summary, error) {
}); err != nil { }); err != nil {
return summary, fmt.Errorf("step %d push: %w", stepIndex, err) return summary, fmt.Errorf("step %d push: %w", stepIndex, err)
} }
if err := options.Verifier.OverrideExtractorValues(v8Overrides); err != nil {
logger.Warn("v8 override apply failed", "step", stepIndex, "err", err)
}
screen := "" screen := ""
if tree != nil && len(tree.Elements) > 0 { if tree != nil && len(tree.Elements) > 0 {
@@ -147,7 +160,13 @@ func Run(ctx context.Context, options Options) (Summary, error) {
} }
} }
nextAction, nextErr := options.Verifier.NextAction() var nextAction verifier.Action
var nextErr error
if web, ok := options.Driver.(driver.WebDriver); ok {
nextAction, nextErr = nextActionFromV8(ctx, web)
} else {
nextAction, nextErr = options.Verifier.NextAction()
}
var traceAction *trace.Action var traceAction *trace.Action
if nextErr == nil { if nextErr == nil {
traceAction = traceActionFor(nextAction, tree) traceAction = traceActionFor(nextAction, tree)
@@ -414,6 +433,62 @@ func captureMetrics(ctx context.Context, options Options, logger *slog.Logger, s
} }
} }
// nextActionFromV8 invokes the V8-side action generator and decodes the
// resulting JSON into a verifier.Action. ErrNoAction is returned when the
// generator declined to act this tick.
func nextActionFromV8(ctx context.Context, web driver.WebDriver) (verifier.Action, error) {
raw, err := web.NextActionFromV8(ctx)
if err != nil {
return verifier.Action{}, fmt.Errorf("v8 next action: %w", err)
}
if len(raw) == 0 || string(raw) == "null" {
return verifier.Action{}, verifier.ErrNoAction
}
var decoded struct {
Kind string `json:"kind"`
X int `json:"x"`
Y int `json:"y"`
FromX int `json:"from_x"`
FromY int `json:"from_y"`
ToX int `json:"to_x"`
ToY int `json:"to_y"`
Key string `json:"key"`
Text string `json:"text"`
DurationMillis int `json:"duration_millis"`
}
if err := json.Unmarshal(raw, &decoded); err != nil {
return verifier.Action{}, fmt.Errorf("decode v8 action: %w", err)
}
switch decoded.Kind {
case "Tap":
if decoded.X == 0 && decoded.Y == 0 {
return verifier.Action{}, verifier.ErrNoAction
}
return verifier.Action{Kind: verifier.ActionKindTap, X: decoded.X, Y: decoded.Y}, nil
case "InputText":
return verifier.Action{
Kind: verifier.ActionKindInputText,
X: decoded.X, Y: decoded.Y,
Text: decoded.Text,
}, nil
case "Swipe":
return verifier.Action{
Kind: verifier.ActionKindSwipe,
FromX: decoded.FromX,
FromY: decoded.FromY,
ToX: decoded.ToX,
ToY: decoded.ToY,
DurationMillis: decoded.DurationMillis,
}, nil
case "PressKey":
return verifier.Action{Kind: verifier.ActionKindPressKey, Key: decoded.Key}, nil
case "Wait":
return verifier.Action{Kind: verifier.ActionKindWait, DurationMillis: decoded.DurationMillis}, nil
default:
return verifier.Action{}, verifier.ErrNoAction
}
}
// captureHTML pulls the current document HTML from a WebDriver-capable // captureHTML pulls the current document HTML from a WebDriver-capable
// driver and stamps it into the trace under html/. Returns true on a // driver and stamps it into the trace under html/. Returns true on a
// successful non-empty write so the Step.HTMLAvailable flag advertises the // successful non-empty write so the Step.HTMLAvailable flag advertises the