diff --git a/eval.go b/eval.go index 5f16c5d0..0a6fbed5 100644 --- a/eval.go +++ b/eval.go @@ -15,27 +15,26 @@ type EvaluateAction Action // Evaluate is an action to evaluate the Javascript expression, unmarshaling // the result of the script evaluation to res. // -// When res is a type other than *[]byte, or **chromedp/cdproto/runtime.RemoteObject, +// When res is a type other than *[]byte, or **runtime.RemoteObject, // then the result of the script evaluation will be returned "by value" (ie, // JSON-encoded), and subsequently an attempt will be made to json.Unmarshal -// the script result to res. +// the script result to res. It returns an error if the script result is +// "undefined" in this case. // // Otherwise, when res is a *[]byte, the raw JSON-encoded value of the script -// result will be placed in res. Similarly, if res is a *runtime.RemoteObject, +// result will be placed in res. Similarly, if res is a **runtime.RemoteObject, // then res will be set to the low-level protocol type, and no attempt will be -// made to convert the result. +// made to convert the result. "undefined" is okay in this case. +// +// When res is nil, the script result will be ignored (including "undefined"). // // Note: any exception encountered will be returned as an error. func Evaluate(expression string, res interface{}, opts ...EvaluateOption) EvaluateAction { - if res == nil { - panic("res cannot be nil") - } - return ActionFunc(func(ctx context.Context) error { // set up parameters p := runtime.Evaluate(expression) switch res.(type) { - case **runtime.RemoteObject: + case nil, **runtime.RemoteObject: default: p = p.WithReturnByValue(true) } @@ -54,6 +53,10 @@ func Evaluate(expression string, res interface{}, opts ...EvaluateOption) Evalua return exp } + if res == nil { + return nil + } + switch x := res.(type) { case **runtime.RemoteObject: *x = v diff --git a/eval_test.go b/eval_test.go new file mode 100644 index 00000000..007dfc61 --- /dev/null +++ b/eval_test.go @@ -0,0 +1,206 @@ +package chromedp + +import ( + "reflect" + "testing" + + "github.com/chromedp/cdproto/runtime" +) + +func TestEvaluateNumber(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expression string + res int + want int + wantErr string + }{ + { + name: "normal", + expression: "123", + want: 123, + }, + { + name: "undefined", + expression: "", + wantErr: "encountered an undefined value", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := testAllocate(t, "") + defer cancel() + + err := Run(ctx, + Evaluate(tt.expression, &tt.res), + ) + if tt.wantErr == "" && err != nil { + t.Fatalf("got error: %v", err) + } + if tt.wantErr != "" && (err == nil || tt.wantErr != err.Error()) { + t.Fatalf("wanted error: %q, got: %q", tt.wantErr, err) + } else if tt.res != tt.want { + t.Fatalf("want: %v, got: %v", tt.want, tt.res) + } + }) + } +} + +func TestEvaluateString(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expression string + res string + want string + wantErr string + }{ + { + name: "normal", + expression: "'str'", + want: "str", + }, + { + name: "undefined", + expression: "", + wantErr: "encountered an undefined value", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := testAllocate(t, "") + defer cancel() + + err := Run(ctx, + Evaluate(tt.expression, &tt.res), + ) + if tt.wantErr == "" && err != nil { + t.Fatalf("got error: %v", err) + } + if tt.wantErr != "" && (err == nil || tt.wantErr != err.Error()) { + t.Fatalf("wanted error: %q, got: %q", tt.wantErr, err) + } else if tt.res != tt.want { + t.Fatalf("want: %v, got: %v", tt.want, tt.res) + } + }) + } +} + +func TestEvaluateBytes(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expression string + res []byte + want []byte + }{ + { + name: "normal", + expression: "'bytes'", + want: []byte(`"bytes"`), + }, + { + name: "undefined", + expression: "", + want: []byte(nil), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := testAllocate(t, "") + defer cancel() + + err := Run(ctx, + Evaluate(tt.expression, &tt.res), + ) + if err != nil { + t.Fatalf("got error: %v", err) + } + if !reflect.DeepEqual(tt.res, tt.want) { + t.Fatalf("want: %v, got: %v", tt.want, tt.res) + } + }) + } +} + +func TestEvaluateRemoteObject(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expression string + res *runtime.RemoteObject + wantType string + }{ + { + name: "object", + expression: "window", + wantType: "object", + }, + { + name: "function", + expression: "window.alert", + wantType: "function", + }, + { + name: "undefined", + expression: "", + wantType: "undefined", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := testAllocate(t, "") + defer cancel() + + err := Run(ctx, + Evaluate(tt.expression, &tt.res), + ) + if err != nil { + t.Fatalf("got error: %v", err) + } + if string(tt.res.Type) != tt.wantType { + t.Fatalf("want type: %v, got type: %v", tt.wantType, tt.res.Type) + } + }) + } +} + +func TestEvaluateNil(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + expression string + }{ + { + name: "number", + expression: "123", + }, + { + name: "string", + expression: "'str'", + }, + { + name: "undefined", + expression: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := testAllocate(t, "") + defer cancel() + + err := Run(ctx, + Evaluate(tt.expression, nil), + ) + if err != nil { + t.Fatalf("got error: %v", err) + } + }) + } +}