From 864094d66c22fb1ff3097a674509a165b7e74a6d Mon Sep 17 00:00:00 2001 From: Zeke Lu Date: Mon, 10 May 2021 15:08:27 +0800 Subject: [PATCH] allow passing nil as res to chromedp.Evaluate() (#816) Sometimes, users just want to call a js function (for example, window.scrollTo(0, 0)), and don't care about the return value. So it's not necessary to force res to not be nil. It also updates the doc comment to describe that in some cases, "undefined" will result in an error. --- eval.go | 21 +++--- eval_test.go | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 eval_test.go 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) + } + }) + } +}