From 0db7a9ee7257b5af9f19245d9765a6c77f2c6995 Mon Sep 17 00:00:00 2001 From: Randy Cahyana Date: Wed, 15 Feb 2017 21:46:07 +0700 Subject: [PATCH] Increasing unit test coverage on query.go --- chromedp.go | 4 +- chromedp_test.go | 99 +++--- handler.go | 50 +-- nav_test.go | 41 +++ query_test.go | 602 ++++++++++++++++++++++++++++++++++++ testdata/form.html | 20 ++ testdata/image.html | 7 + testdata/images/brankas.png | Bin 0 -> 14569 bytes testdata/images/github.png | Bin 0 -> 4268 bytes testdata/table.html | 34 ++ 10 files changed, 782 insertions(+), 75 deletions(-) create mode 100644 nav_test.go create mode 100644 query_test.go create mode 100644 testdata/form.html create mode 100644 testdata/image.html create mode 100644 testdata/images/brankas.png create mode 100644 testdata/images/github.png create mode 100644 testdata/table.html diff --git a/chromedp.go b/chromedp.go index 1baf97b..3490d70 100644 --- a/chromedp.go +++ b/chromedp.go @@ -116,14 +116,14 @@ func (c *CDP) AddTarget(ctxt context.Context, t client.Target) { // create target manager h, err := NewTargetHandler(t, c.logf, c.debugf, c.errorf) if err != nil { - c.errorf("could not create handler for %s, got: %v", t, err) + c.errorf("could not create handler for %s: %v", t, err) return } // run err = h.Run(ctxt) if err != nil { - c.errorf("could not start handler for %s, got: %v", t, err) + c.errorf("could not start handler for %s: %v", t, err) return } diff --git a/chromedp_test.go b/chromedp_test.go index 1aac60e..40d8ef6 100644 --- a/chromedp_test.go +++ b/chromedp_test.go @@ -4,18 +4,63 @@ import ( "context" "log" "os" - "strings" "testing" ) var pool *Pool - var defaultContext = context.Background() +var testdataDir string + +func testAllocate(t *testing.T, path string) *Res { + c, err := pool.Allocate(defaultContext) + if err != nil { + t.Fatalf("could not allocate from pool: %v", err) + } + + err = WithLogf(t.Logf)(c.c) + if err != nil { + t.Fatalf("could not set logf: %v", err) + } + + err = WithDebugf(t.Logf)(c.c) + if err != nil { + t.Fatalf("could not set debugf: %v", err) + } + + err = WithErrorf(t.Errorf)(c.c) + if err != nil { + t.Fatalf("could not set errorf: %v", err) + } + + h := c.c.GetHandlerByIndex(0) + th, ok := h.(*TargetHandler) + if !ok { + t.Fatalf("handler is invalid type") + } + + th.logf = t.Logf + th.debugf = t.Logf + th.errorf = func(s string, v ...interface{}) { + t.Logf("target handler error: "+s, v...) + } + + if path != "" { + err = c.Run(defaultContext, Navigate(testdataDir+"/"+path)) + if err != nil { + t.Fatalf("could not navigate to testdata/%s: %v", path, err) + } + } + + return c +} func TestMain(m *testing.M) { var err error - pool, err = NewPool(PoolLog(log.Printf, log.Printf, log.Printf)) + testdataDir = "file:" + os.Getenv("GOPATH") + "/src/github.com/knq/chromedp/testdata" + + //pool, err = NewPool(PoolLog(log.Printf, log.Printf, log.Printf)) + pool, err = NewPool() if err != nil { log.Fatal(err) } @@ -29,51 +74,3 @@ func TestMain(m *testing.M) { os.Exit(code) } - -func TestNavigate(t *testing.T) { - var err error - - c, err := pool.Allocate(defaultContext) - if err != nil { - t.Fatal(err) - } - defer c.Release() - - err = c.Run(defaultContext, Navigate("https://www.google.com/")) - if err != nil { - t.Fatal(err) - } - - err = c.Run(defaultContext, WaitVisible(`#hplogo`, ByID)) - if err != nil { - t.Fatal(err) - } - - var urlstr string - err = c.Run(defaultContext, Location(&urlstr)) - if err != nil { - t.Fatal(err) - } - if !strings.HasPrefix(urlstr, "https://www.google.") { - t.Errorf("expected to be on google domain, at: %s", urlstr) - } - - var title string - err = c.Run(defaultContext, Title(&title)) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(strings.ToLower(title), "google") { - t.Errorf("expected title to contain google, instead title is: %s", title) - } -} - -func TestSendKeys(t *testing.T) { - var err error - - c, err := pool.Allocate(defaultContext) - if err != nil { - t.Fatal(err) - } - defer c.Release() -} diff --git a/handler.go b/handler.go index 18bf06d..3872bf3 100644 --- a/handler.go +++ b/handler.go @@ -50,7 +50,7 @@ type TargetHandler struct { lastm sync.Mutex // res is the id->result channel map. - res map[int64]chan easyjson.RawMessage + res map[int64]chan *cdp.Message resrw sync.RWMutex // logging funcs @@ -87,7 +87,7 @@ func (h *TargetHandler) Run(ctxt context.Context) error { h.qcmd = make(chan *cdp.Message) h.qres = make(chan *cdp.Message) h.qevents = make(chan *cdp.Message) - h.res = make(map[int64]chan easyjson.RawMessage) + h.res = make(map[int64]chan *cdp.Message) h.detached = make(chan *inspector.EventDetached) h.pageWaitGroup = new(sync.WaitGroup) h.domWaitGroup = new(sync.WaitGroup) @@ -108,7 +108,7 @@ func (h *TargetHandler) Run(ctxt context.Context) error { } { err = a.Do(ctxt, h) if err != nil { - return fmt.Errorf("unable to execute %s, got: %v", reflect.TypeOf(a), err) + return fmt.Errorf("unable to execute %s: %v", reflect.TypeOf(a), err) } } @@ -117,7 +117,7 @@ func (h *TargetHandler) Run(ctxt context.Context) error { // get page resources tree, err := page.GetResourceTree().Do(ctxt, h) if err != nil { - return fmt.Errorf("unable to get resource tree, got: %v", err) + return fmt.Errorf("unable to get resource tree: %v", err) } h.frames[tree.Frame.ID] = tree.Frame @@ -182,19 +182,19 @@ func (h *TargetHandler) run(ctxt context.Context) { case ev := <-h.qevents: err = h.processEvent(ctxt, ev) if err != nil { - h.errorf("could not process event %s, got: %v", ev.Method, err) + h.errorf("could not process event %s: %v", ev.Method, err) } case res := <-h.qres: err = h.processResult(res) if err != nil { - h.errorf("could not process result for message %d, got: %v", res.ID, err) + h.errorf("could not process result for message %d: %v", res.ID, err) } case cmd := <-h.qcmd: err = h.processCommand(cmd) if err != nil { - h.errorf("could not process command message %d, got: %v", cmd.ID, err) + h.errorf("could not process command message %d: %v", cmd.ID, err) } case <-ctxt.Done(): @@ -271,7 +271,7 @@ func (h *TargetHandler) processEvent(ctxt context.Context, msg *cdp.Message) err func (h *TargetHandler) documentUpdated(ctxt context.Context) { f, err := h.WaitFrame(ctxt, EmptyFrameID) if err != nil { - h.errorf("could not get current frame, got: %v", err) + h.errorf("could not get current frame: %v", err) return } @@ -286,7 +286,7 @@ func (h *TargetHandler) documentUpdated(ctxt context.Context) { f.Nodes = make(map[cdp.NodeID]*cdp.Node) f.Root, err = dom.GetDocument().WithPierce(true).Do(ctxt, h) if err != nil { - h.errorf("could not retrieve document root for %s, got: %v", f.ID, err) + h.errorf("could not retrieve document root for %s: %v", f.ID, err) return } f.Root.Invalidated = make(chan struct{}) @@ -304,11 +304,7 @@ func (h *TargetHandler) processResult(msg *cdp.Message) error { } defer close(ch) - if msg.Error != nil { - return msg.Error - } - - ch <- msg.Result + ch <- msg return nil } @@ -346,7 +342,7 @@ func (h *TargetHandler) Execute(ctxt context.Context, commandType cdp.MethodType id := h.next() // save channel - ch := make(chan easyjson.RawMessage, 1) + ch := make(chan *cdp.Message, 1) h.resrw.Lock() h.res[id] = ch h.resrw.Unlock() @@ -364,8 +360,15 @@ func (h *TargetHandler) Execute(ctxt context.Context, commandType cdp.MethodType select { case msg := <-ch: - if res != nil { - errch <- easyjson.Unmarshal(msg, res) + switch { + case msg == nil: + errch <- cdp.ErrChannelClosed + + case msg.Error != nil: + errch <- msg.Error + + case res != nil: + errch <- easyjson.Unmarshal(msg.Result, res) } case <-ctxt.Done(): @@ -559,7 +562,7 @@ func (h *TargetHandler) pageEvent(ctxt context.Context, ev interface{}) { f, err := h.WaitFrame(ctxt, id) if err != nil { - h.errorf("could not get frame %s, got: %v", id, err) + h.errorf("could not get frame %s: %v", id, err) return } @@ -579,7 +582,7 @@ func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) { // wait current frame f, err := h.WaitFrame(ctxt, EmptyFrameID) if err != nil { - h.errorf("error processing DOM event %s: error waiting for frame, got: %v", reflect.TypeOf(ev), err) + h.errorf("could not process DOM event %s: %v", reflect.TypeOf(ev), err) return } @@ -643,12 +646,15 @@ func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) { return } - s := strings.TrimPrefix(strings.TrimSuffix(runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), ".func1"), "github.com/knq/chromedp.") - // retrieve node n, err := h.WaitNode(ctxt, f, id) if err != nil { - h.errorf("error could not perform (%s) operation on node %d (wait node error), got: %v", s, id, err) + s := strings.TrimSuffix(runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), ".func1") + i := strings.LastIndex(s, ".") + if i != -1 { + s = s[i+1:] + } + h.errorf("could not perform (%s) operation on node %d (wait node): %v", s, id, err) return } diff --git a/nav_test.go b/nav_test.go new file mode 100644 index 0000000..9b938f4 --- /dev/null +++ b/nav_test.go @@ -0,0 +1,41 @@ +package chromedp + +import ( + "strings" + "testing" +) + +func TestNavigate(t *testing.T) { + var err error + + c := testAllocate(t, "") + defer c.Release() + + err = c.Run(defaultContext, Navigate("https://www.google.com/")) + if err != nil { + t.Fatal(err) + } + + err = c.Run(defaultContext, WaitVisible(`#hplogo`, ByID)) + if err != nil { + t.Fatal(err) + } + + var urlstr string + err = c.Run(defaultContext, Location(&urlstr)) + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(urlstr, "https://www.google.") { + t.Errorf("expected to be on google domain, at: %s", urlstr) + } + + var title string + err = c.Run(defaultContext, Title(&title)) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(strings.ToLower(title), "google") { + t.Errorf("expected title to contain google, instead title is: %s", title) + } +} diff --git a/query_test.go b/query_test.go new file mode 100644 index 0000000..cbab270 --- /dev/null +++ b/query_test.go @@ -0,0 +1,602 @@ +package chromedp + +import ( + "reflect" + "testing" + "time" + + "github.com/knq/chromedp/cdp" + "github.com/knq/chromedp/cdp/dom" + "github.com/knq/chromedp/kb" +) + +func TestNodes(t *testing.T) { + c := testAllocate(t, "table.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + len int + }{ + {"/html/body/table/tbody[1]/tr[2]/td", BySearch, 3}, + {"body > table > tbody:nth-child(2) > tr:nth-child(2) > td:not(:last-child)", ByQueryAll, 2}, + {"body > table > tbody:nth-child(2) > tr:nth-child(2) > td", ByQuery, 1}, + {"#footer", ByID, 1}, + } + + var err error + for i, test := range tests { + var nodes []*cdp.Node + err = c.Run(defaultContext, Nodes(test.sel, &nodes, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if len(nodes) != test.len { + t.Errorf("test %d expected to have %d nodes: got %d", i, test.len, len(nodes)) + } + } +} + +// TODO: +// NodeIDs +// Focus +// Blur + +func TestDimensions(t *testing.T) { + c := testAllocate(t, "image.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + width int64 + height int64 + }{ + {"/html/body/img", BySearch, 239, 239}, + {"img", ByQueryAll, 239, 239}, + {"img", ByQuery, 239, 239}, + {"#icon-github", ByID, 120, 120}, + } + + var err error + for i, test := range tests { + var model *dom.BoxModel + err = c.Run(defaultContext, Dimensions(test.sel, &model)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if model.Height != test.height || model.Width != test.width { + t.Errorf("test %d expected %dx%d, got: %dx%d", i, test.width, test.height, model.Height, model.Width) + } + } +} + +func TestText(t *testing.T) { + c := testAllocate(t, "form.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + exp string + }{ + {"#foo", ByID, "insert"}, + {"body > form > span", ByQueryAll, "insert"}, + {"body > form > span:nth-child(2)", ByQuery, "keyword"}, + {"/html/body/form/span[2]", BySearch, "keyword"}, + } + + var err error + for i, test := range tests { + var text string + err = c.Run(defaultContext, Text(test.sel, &text, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if text != test.exp { + t.Errorf("test %d expected `%s`, got: %s", i, test.exp, text) + } + } +} + +func TestClear(t *testing.T) { + tests := []struct { + sel string + by QueryOption + }{ + // input fields + {`//*[@id="form"]/input[1]`, BySearch}, + {`#form > input[type="text"]:nth-child(4)`, ByQuery}, + {`#form > input[type="text"]`, ByQueryAll}, + {`#keyword`, ByID}, + + // textarea fields + {`//*[@id="bar"]`, BySearch}, + {`#form > textarea`, ByQuery}, + {`#form > textarea`, ByQueryAll}, + {`#bar`, ByID}, + + // input + textarea fields + {`//*[@id="form"]/input`, BySearch}, + {`#form > input[type="text"]`, ByQueryAll}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "form.html") + defer c.Release() + + var val string + err = c.Run(defaultContext, Value(test.sel, &val, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if val == "" { + t.Errorf("test %d expected `%s` to have non empty value", i, test.sel) + } + + err = c.Run(defaultContext, Clear(test.sel, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + err = c.Run(defaultContext, Value(test.sel, &val, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if val != "" { + t.Errorf("test %d expected empty value for `%s`, got: %s", i, test.sel, val) + } + } +} + +func TestReset(t *testing.T) { + tests := []struct { + sel string + by QueryOption + value string + exp string + }{ + {`//*[@id="keyword"]`, BySearch, "foobar", "chromedp"}, + {`#form > input[type="text"]:nth-child(6)`, ByQuery, "foobar", "foo"}, + {`#form > input[type="text"]`, ByQueryAll, "foobar", "chromedp"}, + {"#bar", ByID, "foobar", "bar"}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "form.html") + defer c.Release() + + err = c.Run(defaultContext, SetValue(test.sel, test.value, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + err = c.Run(defaultContext, Reset(test.sel, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var value string + err = c.Run(defaultContext, Value(test.sel, &value, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if value != test.exp { + t.Errorf("test %d expected value after reset is %s, got: '%s'", i, test.exp, value) + } + } +} + +func TestValue(t *testing.T) { + c := testAllocate(t, "form.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + }{ + {`//*[@id="form"]/input[1]`, BySearch}, + {`#form > input[type="text"]:nth-child(4)`, ByQuery}, + {`#form > input[type="text"]`, ByQueryAll}, + {`#keyword`, ByID}, + } + + var err error + for i, test := range tests { + var value string + err = c.Run(defaultContext, Value(test.sel, &value, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if value != "chromedp" { + t.Errorf("test %d expected `chromedp`, got: %s", i, value) + } + } +} + +func TestSetValue(t *testing.T) { + c := testAllocate(t, "form.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + }{ + {`//*[@id="form"]/input[1]`, BySearch}, + {`#form > input[type="text"]:nth-child(4)`, ByQuery}, + {`#form > input[type="text"]`, ByQueryAll}, + {`#bar`, ByID}, + } + + var err error + for i, test := range tests { + if i != 0 { + err = c.Run(defaultContext, Reload()) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + time.Sleep(50 * time.Millisecond) + } + + err = c.Run(defaultContext, SetValue(test.sel, "FOOBAR", test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var value string + err = c.Run(defaultContext, Value(test.sel, &value, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if value != "FOOBAR" { + t.Errorf("test %d expected `FOOBAR`, got: %s", i, value) + } + } +} + +func TestAttributes(t *testing.T) { + c := testAllocate(t, "image.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + exp map[string]string + }{ + {`//*[@id="icon-brankas"]`, BySearch, + map[string]string{ + "alt": "Brankas - Easy Money Management", + "id": "icon-brankas", + "src": "images/brankas.png", + }}, + {"body > img:first-child", ByQuery, + map[string]string{ + "alt": "Brankas - Easy Money Management", + "id": "icon-brankas", + "src": "images/brankas.png", + }}, + {"body > img:nth-child(2)", ByQueryAll, + map[string]string{ + "alt": `How people build software`, + "id": "icon-github", + "src": "images/github.png", + }}, + {"#icon-github", ByID, + map[string]string{ + "alt": "How people build software", + "id": "icon-github", + "src": "images/github.png", + }}, + } + + var err error + for i, test := range tests { + var attrs map[string]string + err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + if !reflect.DeepEqual(test.exp, attrs) { + t.Errorf("test %d expected %v, got: %v", i, test.exp, attrs) + } + } +} + +func TestSetAttributes(t *testing.T) { + tests := []struct { + sel string + by QueryOption + attrs map[string]string + exp map[string]string + }{ + {`//*[@id="icon-brankas"]`, BySearch, + map[string]string{"data-url": "brankas"}, + map[string]string{ + "alt": "Brankas - Easy Money Management", + "id": "icon-brankas", + "src": "images/brankas.png", + "data-url": "brankas"}}, + {"body > img:first-child", ByQuery, + map[string]string{"data-url": "brankas"}, + map[string]string{ + "alt": "Brankas - Easy Money Management", + "id": "icon-brankas", + "src": "images/brankas.png", + "data-url": "brankas", + }}, + {"body > img:nth-child(2)", ByQueryAll, + map[string]string{"width": "100", "height": "200"}, + map[string]string{ + "alt": `How people build software`, + "id": "icon-github", + "src": "images/github.png", + "width": "100", + "height": "200", + }}, + {"#icon-github", ByID, + map[string]string{"width": "100", "height": "200"}, + map[string]string{ + "alt": "How people build software", + "id": "icon-github", + "src": "images/github.png", + "width": "100", + "height": "200", + }}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "image.html") + defer c.Release() + + err = c.Run(defaultContext, SetAttributes(test.sel, test.attrs, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var attrs map[string]string + err = c.Run(defaultContext, Attributes(test.sel, &attrs, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + if !reflect.DeepEqual(test.exp, attrs) { + t.Errorf("test %d expected %v, got: %v", i, test.exp, attrs) + } + } +} + +func TestAttributeValue(t *testing.T) { + c := testAllocate(t, "image.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + attr string + exp string + }{ + {`//*[@id="icon-brankas"]`, BySearch, "alt", "Brankas - Easy Money Management"}, + {"body > img:first-child", ByQuery, "alt", "Brankas - Easy Money Management"}, + {"body > img:nth-child(2)", ByQueryAll, "alt", "How people build software"}, + {"#icon-github", ByID, "alt", "How people build software"}, + } + + var err error + for i, test := range tests { + var value string + var ok bool + + err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + if !ok { + t.Fatalf("test %d failed to get attribute %s on %s", i, test.attr, test.sel) + } + + if value != test.exp { + t.Errorf("test %d expected %s to be %s, got: %s", i, test.attr, test.exp, value) + } + } +} + +func TestSetAttributeValue(t *testing.T) { + tests := []struct { + sel string + by QueryOption + attr string + exp string + }{ + {`//*[@id="keyword"]`, BySearch, "foo", "bar"}, + {`#form > input[type="text"]:nth-child(6)`, ByQuery, "foo", "bar"}, + {`#form > input[type="text"]`, ByQueryAll, "foo", "bar"}, + {"#bar", ByID, "foo", "bar"}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "form.html") + defer c.Release() + + err = c.Run(defaultContext, SetAttributeValue(test.sel, test.attr, test.exp, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var value string + var ok bool + err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if !ok { + t.Fatalf("test %d failed to get attribute %s on %s", i, test.attr, test.sel) + } + if value != test.exp { + t.Errorf("test %d expected %s to be %s, got: %s", i, test.attr, test.exp, value) + } + } +} + +func TestRemoveAttribute(t *testing.T) { + c := testAllocate(t, "image.html") + defer c.Release() + + tests := []struct { + sel string + by QueryOption + attr string + }{ + {"/html/body/img", BySearch, "alt"}, + {"img", ByQueryAll, "alt"}, + {"img", ByQuery, "alt"}, + {"#icon-github", ByID, "alt"}, + } + + var err error + for i, test := range tests { + if i != 0 { + err = c.Run(defaultContext, Reload()) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + time.Sleep(50 * time.Millisecond) + } + + err = c.Run(defaultContext, RemoveAttribute(test.sel, test.attr)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var value string + var ok bool + err = c.Run(defaultContext, AttributeValue(test.sel, test.attr, &value, &ok, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if ok || value != "" { + t.Fatalf("test %d expected attribute %s removed from element %s", i, test.attr, test.sel) + } + } +} + +func TestClick(t *testing.T) { + tests := []struct { + sel string + by QueryOption + }{ + {`//*[@id="form"]/input[4]`, BySearch}, + {`#form > input[type="submit"]:nth-child(11)`, ByQuery}, + {`#form > input[type="submit"]:nth-child(11)`, ByQueryAll}, + {"#btn2", ByID}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "form.html") + defer c.Release() + + err = c.Run(defaultContext, Click(test.sel, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + err = c.Run(defaultContext, Sleep(time.Second*2)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var title string + err = c.Run(defaultContext, Title(&title)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if title != "chromedp - Google Search" { + t.Errorf("test %d expected title to be 'chromedp - Google Search', got: '%s'", i, title) + } + } +} + +// DoubleClick + +func TestSendKeys(t *testing.T) { + tests := []struct { + sel string + by QueryOption + keys string + exp string + }{ + {`//*[@id="input1"]`, BySearch, "INSERT ", "INSERT some value"}, + {`#box4 > input:nth-child(1)`, ByQuery, "insert ", "insert some value"}, + {`#box4 > textarea`, ByQueryAll, "prefix " + kb.End + "\b\b SUFFIX\n", "prefix textar SUFFIX\n"}, + {"#textarea1", ByID, "insert ", "insert textarea"}, + {"#textarea1", ByID, kb.End + "\b\b\n\naoeu\n\nfoo\n\nbar\n\n", "textar\n\naoeu\n\nfoo\n\nbar\n\n"}, + {"#select1", ByID, kb.ArrowDown + kb.ArrowDown, "three"}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "visible.html") + defer c.Release() + + err = c.Run(defaultContext, SendKeys(test.sel, test.keys, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var val string + err = c.Run(defaultContext, Value(test.sel, &val, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if val != test.exp { + t.Errorf("test %d expected value %s, got: %s", i, test.exp, val) + } + } +} + +func TestSubmit(t *testing.T) { + tests := []struct { + sel string + by QueryOption + }{ + {`//*[@id="keyword"]`, BySearch}, + {`#form > input[type="text"]:nth-child(4)`, ByQuery}, + {`#form > input[type="text"]`, ByQueryAll}, + {"#form", ByID}, + } + + var err error + for i, test := range tests { + c := testAllocate(t, "form.html") + defer c.Release() + + err = c.Run(defaultContext, Submit(test.sel, test.by)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + err = c.Run(defaultContext, Sleep(time.Second*2)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + + var title string + err = c.Run(defaultContext, Title(&title)) + if err != nil { + t.Fatalf("test %d got error: %v", i, err) + } + if title != "chromedp - Google Search" { + t.Errorf("test %d expected title to be 'chromedp - Google Search', got: '%s'", i, title) + } + } +} + +// ComputedStyle +// MatchedStyle diff --git a/testdata/form.html b/testdata/form.html new file mode 100644 index 0000000..715b467 --- /dev/null +++ b/testdata/form.html @@ -0,0 +1,20 @@ + + + + + + +
+ insert keyword
+
+
+
+ + +
+ + \ No newline at end of file diff --git a/testdata/image.html b/testdata/image.html new file mode 100644 index 0000000..910c9b5 --- /dev/null +++ b/testdata/image.html @@ -0,0 +1,7 @@ + + + + Brankas - Easy Money Management + How people build software + + \ No newline at end of file diff --git a/testdata/images/brankas.png b/testdata/images/brankas.png new file mode 100644 index 0000000000000000000000000000000000000000..59a8f711462af5a484f67f9a3d6002d613e1cd79 GIT binary patch literal 14569 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 literal 0 HcmV?d00001 diff --git a/testdata/images/github.png b/testdata/images/github.png new file mode 100644 index 0000000000000000000000000000000000000000..ea6ff545a246caa64074ba809bbc86fcb8589071 GIT binary patch literal 4268 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 XLyRvu{aX3a{{tgEGu={c*U0|?Dtn-9 literal 0 HcmV?d00001 diff --git a/testdata/table.html b/testdata/table.html new file mode 100644 index 0000000..bb43b4a --- /dev/null +++ b/testdata/table.html @@ -0,0 +1,34 @@ + + + + table + + + + + + + + + + + + + + + + + + + + + + + + + + + +
row 1 headerrow 2 headerrow 3 header
1.11.21.3
2.12.22.3
+ + \ No newline at end of file