diff --git a/examples/upload/main.go b/examples/upload/main.go index e042ecf..3e0bc44 100644 --- a/examples/upload/main.go +++ b/examples/upload/main.go @@ -10,8 +10,6 @@ import ( "os" cdp "github.com/knq/chromedp" - cdptypes "github.com/knq/chromedp/cdp" - "github.com/knq/chromedp/cdp/dom" ) var ( @@ -97,14 +95,9 @@ func main() { } func upload(filepath string, sz *string) cdp.Tasks { - var ids []cdptypes.NodeID return cdp.Tasks{ cdp.Navigate(fmt.Sprintf("http://localhost:%d", *flagPort)), - cdp.WaitVisible(`input[name="upload"]`), - cdp.NodeIDs(`input[name="upload"]`, &ids, cdp.NodeVisible), - cdp.ActionFunc(func(ctxt context.Context, h cdptypes.Handler) error { - return dom.SetFileInputFiles(ids[0], []string{filepath}).Do(ctxt, h) - }), + cdp.SendKeys(`input[name="upload"]`, filepath, cdp.NodeVisible), cdp.Click(`input[name="submit"]`), cdp.Text(`#result`, sz, cdp.ByID, cdp.NodeVisible), } diff --git a/input.go b/input.go index 91ae324..3a9c3d9 100644 --- a/input.go +++ b/input.go @@ -147,8 +147,10 @@ func ClickCount(n int) MouseOption { // KeyAction will synthesize a keyDown, char, and keyUp event for each rune // contained in keys along with any supplied key options. // -// Note: only well known, "printable" characters will have "char" events -// synthesized. +// Only well-known, "printable" characters will have char events synthesized. +// +// Please see the chromedp/kb package for implementation details and the list +// of well-known keys. func KeyAction(keys string, opts ...KeyOption) Action { return ActionFunc(func(ctxt context.Context, h cdp.Handler) error { var err error diff --git a/query.go b/query.go index 2f6b349..f335682 100644 --- a/query.go +++ b/query.go @@ -329,15 +329,48 @@ func DoubleClick(sel interface{}, opts ...QueryOption) Action { // SendKeys synthesizes the key up, char, and down events as needed for the // runes in v, sending them to the first node matching the selector. +// +// Note: when selector matches a input[type="file"] node, then dom.SetFileInputFiles +// is used to set the upload path of the input node to v. func SendKeys(sel interface{}, v string, opts ...QueryOption) Action { return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { if len(nodes) < 1 { return fmt.Errorf("selector `%s` did not return any nodes", sel) } - return KeyActionNode(nodes[0], v).Do(ctxt, h) + + n := nodes[0] + + // grab type attribute from node + typ, attrs := "", n.Attributes + n.RLock() + for i := 0; i < len(attrs); i += 2 { + if attrs[i] == "type" { + typ = attrs[i+1] + } + } + n.RUnlock() + + // when working with input[type="file"], call dom.SetFileInputFiles + if n.NodeName == "INPUT" && typ == "file" { + return dom.SetFileInputFiles(n.NodeID, []string{v}).Do(ctxt, h) + } + + return KeyActionNode(n, v).Do(ctxt, h) }, append(opts, NodeVisible)...) } +// SetUploadFiles sets the files to upload (ie, for a input[type="file"] node) +// for the first node matching the selector. +func SetUploadFiles(sel interface{}, files []string, opts ...QueryOption) Action { + return QueryAfter(sel, func(ctxt context.Context, h cdp.Handler, nodes ...*cdp.Node) error { + if len(nodes) < 1 { + return fmt.Errorf("selector `%s` did not return any nodes", sel) + } + + return dom.SetFileInputFiles(nodes[0].NodeID, files).Do(ctxt, h) + }, opts...) +} + // Screenshot takes a screenshot of the first node matching the selector. func Screenshot(sel interface{}, picbuf *[]byte, opts ...QueryOption) Action { if picbuf == nil { diff --git a/query_test.go b/query_test.go index 6a718f1..ada9ba9 100644 --- a/query_test.go +++ b/query_test.go @@ -2,6 +2,10 @@ package chromedp import ( "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" "reflect" "testing" "time" @@ -853,3 +857,96 @@ func TestMatchedStyle(t *testing.T) { }) } } + +func TestFileUpload(t *testing.T) { + t.Parallel() + + // create test server + mux := http.NewServeMux() + mux.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { + fmt.Fprintf(res, uploadHTML) + }) + mux.HandleFunc("/upload", func(res http.ResponseWriter, req *http.Request) { + f, _, err := req.FormFile("upload") + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } + defer f.Close() + + buf, err := ioutil.ReadAll(f) + if err != nil { + http.Error(res, err.Error(), http.StatusBadRequest) + return + } + + fmt.Fprintf(res, resultHTML, len(buf)) + }) + s := httptest.NewServer(mux) + defer s.Close() + + // create temporary file on disk + tmpfile, err := ioutil.TempFile("", "chromedp-upload-test") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + if _, err = tmpfile.WriteString(uploadHTML); err != nil { + t.Fatal(err) + } + if err = tmpfile.Close(); err != nil { + t.Fatal(err) + } + + c := testAllocate(t, "") + defer c.Release() + + tests := []struct { + a Action + }{ + {SendKeys(`input[name="upload"]`, tmpfile.Name(), NodeVisible)}, + {SetUploadFiles(`input[name="upload"]`, []string{tmpfile.Name()}, NodeVisible)}, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) { + test = test + + c := testAllocate(t, "") + defer c.Release() + + var result string + err = c.Run(defaultContext, Tasks{ + Navigate(s.URL), + test.a, + Click(`input[name="submit"]`), + Text(`#result`, &result, ByID, NodeVisible), + }) + if err != nil { + t.Fatalf("test %d expected no error, got: %v", i, err) + } + if result != fmt.Sprintf("%d", len(uploadHTML)) { + t.Errorf("test %d expected result to be %d, got: %s", i, len(uploadHTML), result) + } + }) + } +} + +const ( + uploadHTML = ` + + +
+ + +
+ +` + + resultHTML = ` + + +
%d
+ +` +)