mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
fix(web): install lastAction in the page before extractors read it
state.lastAction was hardcoded null on web, so every property reading it was silently vacuous: a correct property passed without ever firing.
This commit is contained in:
1 parent
df74bf4d6f
commit
1d46107ee8
4 files changed
+122
-4
No files matched your search
@@ -144,8 +144,11 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
var v8Overrides map[int]json.RawMessage
|
var v8Overrides map[int]json.RawMessage
|
||||||
|
// The same lastAction PushSnapshot hands the goja state below: the two
|
||||||
|
// engines evaluate this step against one action, not two.
|
||||||
|
stepAction := lastAction
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
overrides, err := extractorSource.ExtractorOverrides(gctx)
|
overrides, err := extractorSource.ExtractorOverrides(gctx, stepAction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("v8 extractor evaluation failed", "step", si, "err", err)
|
logger.Warn("v8 extractor evaluation failed", "step", si, "err", err)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -23,8 +23,23 @@ type ActionSource interface {
|
|||||||
// ExtractorSource yields per-step extractor overrides the runner applies after
|
// ExtractorSource yields per-step extractor overrides the runner applies after
|
||||||
// PushSnapshot. The mobile path has none (returns nil); the web path returns the
|
// PushSnapshot. The mobile path has none (returns nil); the web path returns the
|
||||||
// values its extractors computed in V8 against the real DOM.
|
// values its extractors computed in V8 against the real DOM.
|
||||||
|
//
|
||||||
|
// lastAction is the action the previous step actually applied, the same value
|
||||||
|
// PushSnapshot hands the goja state. The web path has to install it in the page
|
||||||
|
// before its extractors run: a spec extractor reading state.lastAction runs in
|
||||||
|
// V8 there, and V8 has no way to know what the runner dispatched.
|
||||||
type ExtractorSource interface {
|
type ExtractorSource interface {
|
||||||
ExtractorOverrides(ctx context.Context) (map[int]json.RawMessage, error)
|
ExtractorOverrides(
|
||||||
|
ctx context.Context,
|
||||||
|
lastAction *verifier.Action,
|
||||||
|
) (map[int]json.RawMessage, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastActionInstaller is the web driver's channel for the previous step's
|
||||||
|
// action. It is declared here rather than folded into driver.WebDriver so the
|
||||||
|
// mobile drivers stay untouched; every web driver must implement it.
|
||||||
|
type lastActionInstaller interface {
|
||||||
|
SetLastAction(ctx context.Context, encoded json.RawMessage) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// gojaSource drives both action selection and (trivially) extractor overrides
|
// gojaSource drives both action selection and (trivially) extractor overrides
|
||||||
@@ -38,7 +53,10 @@ func (s gojaSource) NextAction(context.Context) (verifier.Action, error) {
|
|||||||
return s.verifier.NextAction()
|
return s.verifier.NextAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gojaSource) ExtractorOverrides(context.Context) (map[int]json.RawMessage, error) {
|
func (gojaSource) ExtractorOverrides(
|
||||||
|
context.Context,
|
||||||
|
*verifier.Action,
|
||||||
|
) (map[int]json.RawMessage, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +77,24 @@ func (s webSource) NextAction(ctx context.Context) (verifier.Action, error) {
|
|||||||
return verifier.DecodeAction(raw)
|
return verifier.DecodeAction(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s webSource) ExtractorOverrides(ctx context.Context) (map[int]json.RawMessage, error) {
|
// ExtractorOverrides installs the previous step's action in the page, then
|
||||||
|
// reads back what the spec's extractors computed against the live DOM. The
|
||||||
|
// install is not best-effort: a web driver that cannot take it leaves
|
||||||
|
// state.lastAction null in V8, which silently turns every action-gated
|
||||||
|
// property vacuously true, so it is reported as an error instead.
|
||||||
|
func (s webSource) ExtractorOverrides(
|
||||||
|
ctx context.Context,
|
||||||
|
lastAction *verifier.Action,
|
||||||
|
) (map[int]json.RawMessage, error) {
|
||||||
|
installer, ok := s.web.(lastActionInstaller)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"web driver %T cannot install state.lastAction; every property gated "+
|
||||||
|
"on the last action would be vacuously true", s.web)
|
||||||
|
}
|
||||||
|
if err := installer.SetLastAction(ctx, verifier.EncodeLastAction(lastAction)); err != nil {
|
||||||
|
return nil, fmt.Errorf("install last action: %w", err)
|
||||||
|
}
|
||||||
return s.web.EvaluateExtractors(ctx)
|
return s.web.EvaluateExtractors(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ func (d *webMockDriver) NextActionFromV8(context.Context) (json.RawMessage, erro
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *webMockDriver) SetLastAction(context.Context, json.RawMessage) error { return nil }
|
||||||
|
|
||||||
// TestRunner_TraceRecordsTheValueTheVerdictUsed fails if the trace and the
|
// TestRunner_TraceRecordsTheValueTheVerdictUsed fails if the trace and the
|
||||||
// verdict disagree about an extractor. A witness is only an explanation of a
|
// verdict disagree about an extractor. A witness is only an explanation of a
|
||||||
// violation if it holds the state the violated property was evaluated against.
|
// violation if it holds the state the violated property was evaluated against.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// On web the spec's extractors run in the page, so state.lastAction has to be
|
||||||
|
// installed there by the runner. It used to be hardcoded null in
|
||||||
|
// pkg/spec/src/web-runtime.ts, which made every property gated on the last
|
||||||
|
// action (folio's submitMovesBalanceByTypedAmount, for one) vacuously true on
|
||||||
|
// web: no failure, no warning, just a green run that proved nothing.
|
||||||
|
|
||||||
|
const lastActionSpec = `
|
||||||
|
import { actions } from "@sanderling/spec";
|
||||||
|
globalThis.actions = actions(() => []);
|
||||||
|
globalThis.properties = {};
|
||||||
|
`
|
||||||
|
|
||||||
|
// tappingWebDriver is a web target whose V8 picker always taps one named
|
||||||
|
// control, so the runner has a real applied action to report on the next step.
|
||||||
|
type tappingWebDriver struct {
|
||||||
|
*mockdriver.Driver
|
||||||
|
installed []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *tappingWebDriver) InstallBundle(context.Context, []byte) error { return nil }
|
||||||
|
|
||||||
|
func (d *tappingWebDriver) EvaluateExtractors(context.Context) (map[int]json.RawMessage, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *tappingWebDriver) NextActionFromV8(context.Context) (json.RawMessage, error) {
|
||||||
|
return json.RawMessage(`{"kind":"Tap","x":12,"y":34,"selector":"id:TxnSubmit"}`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *tappingWebDriver) SetLastAction(_ context.Context, encoded json.RawMessage) error {
|
||||||
|
d.installed = append(d.installed, string(encoded))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunner_WebInstallsLastActionInThePage(t *testing.T) {
|
||||||
|
state := newHarnessWithSpec(t, lastActionSpec)
|
||||||
|
web := &tappingWebDriver{Driver: state.mock}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := Run(ctx, Options{
|
||||||
|
Duration: 100 * time.Millisecond,
|
||||||
|
IdleTimeout: 20 * time.Millisecond,
|
||||||
|
MaxSteps: 3,
|
||||||
|
Driver: web,
|
||||||
|
Verifier: state.verifier,
|
||||||
|
TraceWriter: state.writer,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("Run: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(web.installed) < 2 {
|
||||||
|
t.Fatalf("the page was handed lastAction %d time(s); the web path never installed it",
|
||||||
|
len(web.installed))
|
||||||
|
}
|
||||||
|
// Step 1 has no previous action, exactly as the goja host reports it.
|
||||||
|
if web.installed[0] != "null" {
|
||||||
|
t.Errorf("step 1 installed %s, want null", web.installed[0])
|
||||||
|
}
|
||||||
|
// Every later step carries what the runner actually applied. The shape is
|
||||||
|
// the goja host's (internal/verifier/marshal.go lastActionFields), pinned
|
||||||
|
// against it by TestLastAction_WebJSONMatchesTheGojaObject.
|
||||||
|
const want = `{"kind":"Tap","on":"id:TxnSubmit"}`
|
||||||
|
if web.installed[1] != want {
|
||||||
|
t.Errorf("step 2 installed %s, want %s", web.installed[1], want)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user