Increasing unit test coverage on query.go
This commit is contained in:
parent
dc08ecc727
commit
0db7a9ee72
|
@ -116,14 +116,14 @@ func (c *CDP) AddTarget(ctxt context.Context, t client.Target) {
|
||||||
// create target manager
|
// create target manager
|
||||||
h, err := NewTargetHandler(t, c.logf, c.debugf, c.errorf)
|
h, err := NewTargetHandler(t, c.logf, c.debugf, c.errorf)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// run
|
// run
|
||||||
err = h.Run(ctxt)
|
err = h.Run(ctxt)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,63 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
var pool *Pool
|
var pool *Pool
|
||||||
|
|
||||||
var defaultContext = context.Background()
|
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) {
|
func TestMain(m *testing.M) {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -29,51 +74,3 @@ func TestMain(m *testing.M) {
|
||||||
|
|
||||||
os.Exit(code)
|
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()
|
|
||||||
}
|
|
||||||
|
|
50
handler.go
50
handler.go
|
@ -50,7 +50,7 @@ type TargetHandler struct {
|
||||||
lastm sync.Mutex
|
lastm sync.Mutex
|
||||||
|
|
||||||
// res is the id->result channel map.
|
// res is the id->result channel map.
|
||||||
res map[int64]chan easyjson.RawMessage
|
res map[int64]chan *cdp.Message
|
||||||
resrw sync.RWMutex
|
resrw sync.RWMutex
|
||||||
|
|
||||||
// logging funcs
|
// logging funcs
|
||||||
|
@ -87,7 +87,7 @@ func (h *TargetHandler) Run(ctxt context.Context) error {
|
||||||
h.qcmd = make(chan *cdp.Message)
|
h.qcmd = make(chan *cdp.Message)
|
||||||
h.qres = make(chan *cdp.Message)
|
h.qres = make(chan *cdp.Message)
|
||||||
h.qevents = 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.detached = make(chan *inspector.EventDetached)
|
||||||
h.pageWaitGroup = new(sync.WaitGroup)
|
h.pageWaitGroup = new(sync.WaitGroup)
|
||||||
h.domWaitGroup = 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)
|
err = a.Do(ctxt, h)
|
||||||
if err != nil {
|
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
|
// get page resources
|
||||||
tree, err := page.GetResourceTree().Do(ctxt, h)
|
tree, err := page.GetResourceTree().Do(ctxt, h)
|
||||||
if err != nil {
|
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
|
h.frames[tree.Frame.ID] = tree.Frame
|
||||||
|
@ -182,19 +182,19 @@ func (h *TargetHandler) run(ctxt context.Context) {
|
||||||
case ev := <-h.qevents:
|
case ev := <-h.qevents:
|
||||||
err = h.processEvent(ctxt, ev)
|
err = h.processEvent(ctxt, ev)
|
||||||
if err != nil {
|
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:
|
case res := <-h.qres:
|
||||||
err = h.processResult(res)
|
err = h.processResult(res)
|
||||||
if err != nil {
|
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:
|
case cmd := <-h.qcmd:
|
||||||
err = h.processCommand(cmd)
|
err = h.processCommand(cmd)
|
||||||
if err != nil {
|
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():
|
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) {
|
func (h *TargetHandler) documentUpdated(ctxt context.Context) {
|
||||||
f, err := h.WaitFrame(ctxt, EmptyFrameID)
|
f, err := h.WaitFrame(ctxt, EmptyFrameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.errorf("could not get current frame, got: %v", err)
|
h.errorf("could not get current frame: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ func (h *TargetHandler) documentUpdated(ctxt context.Context) {
|
||||||
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
|
f.Nodes = make(map[cdp.NodeID]*cdp.Node)
|
||||||
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctxt, h)
|
f.Root, err = dom.GetDocument().WithPierce(true).Do(ctxt, h)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
f.Root.Invalidated = make(chan struct{})
|
f.Root.Invalidated = make(chan struct{})
|
||||||
|
@ -304,11 +304,7 @@ func (h *TargetHandler) processResult(msg *cdp.Message) error {
|
||||||
}
|
}
|
||||||
defer close(ch)
|
defer close(ch)
|
||||||
|
|
||||||
if msg.Error != nil {
|
ch <- msg
|
||||||
return msg.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
ch <- msg.Result
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -346,7 +342,7 @@ func (h *TargetHandler) Execute(ctxt context.Context, commandType cdp.MethodType
|
||||||
id := h.next()
|
id := h.next()
|
||||||
|
|
||||||
// save channel
|
// save channel
|
||||||
ch := make(chan easyjson.RawMessage, 1)
|
ch := make(chan *cdp.Message, 1)
|
||||||
h.resrw.Lock()
|
h.resrw.Lock()
|
||||||
h.res[id] = ch
|
h.res[id] = ch
|
||||||
h.resrw.Unlock()
|
h.resrw.Unlock()
|
||||||
|
@ -364,8 +360,15 @@ func (h *TargetHandler) Execute(ctxt context.Context, commandType cdp.MethodType
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case msg := <-ch:
|
case msg := <-ch:
|
||||||
if res != nil {
|
switch {
|
||||||
errch <- easyjson.Unmarshal(msg, res)
|
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():
|
case <-ctxt.Done():
|
||||||
|
@ -559,7 +562,7 @@ func (h *TargetHandler) pageEvent(ctxt context.Context, ev interface{}) {
|
||||||
|
|
||||||
f, err := h.WaitFrame(ctxt, id)
|
f, err := h.WaitFrame(ctxt, id)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -579,7 +582,7 @@ func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) {
|
||||||
// wait current frame
|
// wait current frame
|
||||||
f, err := h.WaitFrame(ctxt, EmptyFrameID)
|
f, err := h.WaitFrame(ctxt, EmptyFrameID)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -643,12 +646,15 @@ func (h *TargetHandler) domEvent(ctxt context.Context, ev interface{}) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := strings.TrimPrefix(strings.TrimSuffix(runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), ".func1"), "github.com/knq/chromedp.")
|
|
||||||
|
|
||||||
// retrieve node
|
// retrieve node
|
||||||
n, err := h.WaitNode(ctxt, f, id)
|
n, err := h.WaitNode(ctxt, f, id)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
41
nav_test.go
Normal file
41
nav_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
602
query_test.go
Normal file
602
query_test.go
Normal file
|
@ -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
|
20
testdata/form.html
vendored
Normal file
20
testdata/form.html
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
input, textarea {
|
||||||
|
margin-top: 5px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<form id="form" action="https://www.google.com/search" name="form">
|
||||||
|
<span id="foo">insert</span> <span>keyword</span><br>
|
||||||
|
<input id="keyword" type="text" name="q" value="chromedp"/><br>
|
||||||
|
<input id="foo" type="text" name="foo" value="foo"/><br>
|
||||||
|
<textarea id="bar" rows="4" cols="50">bar</textarea><br>
|
||||||
|
<input id="btn1" type="reset" value="Reset">
|
||||||
|
<input id="btn2" type="submit" value="Submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
7
testdata/image.html
vendored
Normal file
7
testdata/image.html
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body style="background-color: #41a1e1">
|
||||||
|
<img id="icon-brankas" src="images/brankas.png" alt="Brankas - Easy Money Management">
|
||||||
|
<img id="icon-github" src="images/github.png" alt="How people build software">
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
testdata/images/brankas.png
vendored
Normal file
BIN
testdata/images/brankas.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
testdata/images/github.png
vendored
Normal file
BIN
testdata/images/github.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
34
testdata/table.html
vendored
Normal file
34
testdata/table.html
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>table</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>row 1 header</th>
|
||||||
|
<th>row 2 header</th>
|
||||||
|
<th>row 3 header</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>1.1</td>
|
||||||
|
<td>1.2</td>
|
||||||
|
<td>1.3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>2.1</td>
|
||||||
|
<td>2.2</td>
|
||||||
|
<td>2.3</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="footer">
|
||||||
|
<td>3.1</td>
|
||||||
|
<td>3.2</td>
|
||||||
|
<td>3.3</td>
|
||||||
|
</tr>
|
||||||
|
<tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user