diff --git a/README.md b/README.md index f647439..c28e220 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -# chromedp-helper +[godoc]: https://godoc.org/github.com/go-oss/chromedp-helper +[godoc-badge]: https://godoc.org/github.com/go-oss/chromedp-helper?status.svg +[github-actions-badge]: https://github.com/go-oss/chromedp-helper/workflows/Test%20on%20master/badge.svg -![](https://github.com/go-oss/chromedp-helper/workflows/Test%20on%20master/badge.svg) +# chromedp-helper ![][github-actions-badge] [![][godoc-badge]][godoc] [chromedp](https://github.com/chromedp/chromedp) helper utilities. diff --git a/go.mod b/go.mod index 43f404b..51af345 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,9 @@ go 1.14 require ( github.com/chromedp/cdproto v0.0.0-20200424080200-0de008e41fa0 github.com/chromedp/chromedp v0.5.3 + github.com/gobwas/ws v1.0.3 // indirect + github.com/mailru/easyjson v0.7.1 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b + golang.org/x/sys v0.0.0-20200428200454-593003d681fa // indirect + golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 // indirect ) diff --git a/go.sum b/go.sum index e322fe7..6d99950 100644 --- a/go.sum +++ b/go.sum @@ -9,23 +9,40 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.0.3 h1:ZOigqf7iBxkA4jdQ3am7ATzdlOFp9YzA6NmuvEEZc9g= +github.com/gobwas/ws v1.0.3/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs= github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0= github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200428200454-593003d681fa h1:yMbJOvnfYkO1dSAviTu/ZguZWLBTXx4xE3LYrxUCCiA= +golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32 h1:Xvf3ZQTm5bjXPxhI7g+dwqsCqadK1rcNtwtszuatetk= +golang.org/x/tools v0.0.0-20200428211428-0c9eba77bc32/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/helper.go b/helper.go index 6b8fee9..d6ca686 100644 --- a/helper.go +++ b/helper.go @@ -26,7 +26,9 @@ var ( ) // Screenshot is an action that takes a screenshot of the entire browser viewport and save as image file. +// // Note: this will override the viewport emulation settings. +// // This function is based on https://github.com/chromedp/examples func Screenshot(filename string) chromedp.Action { return chromedp.ActionFunc(func(ctx context.Context) error { @@ -94,10 +96,20 @@ func Navigate(urlstr interface{}, timeout time.Duration) chromedp.NavigateAction // IgnoreCacheReload is an action that reloads the current page without cache. func IgnoreCacheReload(timeout time.Duration) chromedp.NavigateAction { return chromedp.ActionFunc(func(ctx context.Context) error { - if err := page.Reload().WithIgnoreCache(true).Do(ctx); err != nil { + _, entries, err := page.GetNavigationHistory().Do(ctx) + if err != nil { return err } - return WaitLoaded(timeout).Do(ctx) + currentURL := entries[len(entries)-1].URL + log.Printf("IgnoreCacheReload: current=%s\n", currentURL) + return WaitResponse(currentURL, timeout, + chromedp.ActionFunc(func(ctx context.Context) error { + if err := page.Reload().WithIgnoreCache(true).Do(ctx); err != nil { + return err + } + return nil + }), + ).Do(ctx) }) } @@ -195,6 +207,9 @@ func WaitResponse(urlstr interface{}, timeout time.Duration, acts ...chromedp.Ac } log.Printf("WaitResponse: timeout=%s\n", timeout) timer := time.NewTimer(timeout) + defer timer.Stop() + ticker := time.NewTicker(time.Second) + defer ticker.Stop() for { select { case err, open := <-ch: @@ -203,7 +218,7 @@ func WaitResponse(urlstr interface{}, timeout time.Duration, acts ...chromedp.Ac } if open { log.Println("WaitResponse: reload") - time.Sleep(time.Second) + <-timer.C reload := page.Reload().WithIgnoreCache(true) if err := reload.Do(ctx); err != nil { return err @@ -235,6 +250,7 @@ func WaitLoaded(timeout time.Duration) chromedp.Action { }) log.Printf("WaitLoaded: timeout=%s\n", timeout) timer := time.NewTimer(timeout) + defer timer.Stop() select { case <-ch: return nil diff --git a/helper_test.go b/helper_test.go index 9490096..8f61b4a 100644 --- a/helper_test.go +++ b/helper_test.go @@ -4,33 +4,246 @@ import ( "bytes" "context" "errors" + "fmt" + "image" + _ "image/png" "io" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "sync" "testing" "time" + + "github.com/chromedp/cdproto/network" + cdpruntime "github.com/chromedp/cdproto/runtime" + "github.com/chromedp/chromedp" +) + +var ( + allocOpts = append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.DisableGPU, chromedp.NoSandbox) + browserOpts []chromedp.ContextOption + allocateOnce sync.Once + startServerOnce sync.Once + allocCtx context.Context + browserCtx context.Context + testServer *httptest.Server + testdataDir string + testdataURL string ) +func init() { + wd, err := os.Getwd() + if err != nil { + panic(fmt.Sprintf("could not get working directory: %v", err)) + } + testdataDir = filepath.Join(wd, "testdata") + testdataURL = "file://" + path.Join(wd, "testdata") +} + +func TestMain(m *testing.M) { + var cancel context.CancelFunc + allocCtx, cancel = chromedp.NewExecAllocator(context.Background(), allocOpts...) + + if debug := os.Getenv("DEBUG"); debug != "" && debug != "false" { + browserOpts = append(browserOpts, chromedp.WithDebugf(log.Printf)) + } + + code := m.Run() + cancel() + + if testServer != nil { + testServer.Close() + } + + os.Exit(code) +} + +func testAllocate(tb testing.TB) (context.Context, context.CancelFunc) { + allocateOnce.Do(func() { + ctx, _ := chromedp.NewContext(allocCtx, browserOpts...) + if err := chromedp.Run(ctx); err != nil { + tb.Fatal(err) + } + chromedp.ListenBrowser(ctx, func(ev interface{}) { + switch ev := ev.(type) { + case *cdpruntime.EventExceptionThrown: + tb.Errorf("%+v\n", ev.ExceptionDetails) + } + }) + browserCtx = ctx + }) + + if browserCtx == nil { + tb.FailNow() + } + + // Create new tab of existing browser. + ctx, _ := chromedp.NewContext(browserCtx) + cancel := func() { + if err := chromedp.Cancel(ctx); err != nil { + tb.Error(err) + } + } + return ctx, cancel +} + +func testStartServer(tb testing.TB) string { + startServerOnce.Do(func() { + mux := http.NewServeMux() + mux.HandleFunc("/image.png", func(w http.ResponseWriter, r *http.Request) { + time.Sleep(2 * time.Second) + http.ServeFile(w, r, filepath.Join(testdataDir, "image.png")) + }) + mux.Handle("/", http.FileServer(http.Dir(testdataDir))) + testServer = httptest.NewServer(mux) + }) + + if testServer == nil { + tb.FailNow() + } + + return testServer.URL +} + func TestScreenshot(t *testing.T) { - t.Skip("TODO") + t.Parallel() + ctx, cancel := testAllocate(t) + defer cancel() + + dir, err := ioutil.TempDir("", "chromedp-helper-test") + if err != nil { + t.Fatalf("failed to create temporary directory: %v", err) + } + sspath := filepath.Join(dir, "screenshot.png") + log.Println("sspath:", sspath) + + tasks := chromedp.Tasks{ + chromedp.Navigate(testdataURL + "/screenshot.html"), + Screenshot(sspath), + } + if err := chromedp.Run(ctx, tasks); err != nil { + t.Fatal(err) + } + + f, err := os.Open(sspath) + if err != nil { + t.Fatalf("failed to open screenshot file: %v", err) + } + defer f.Close() + + config, format, err := image.DecodeConfig(f) + if err != nil { + t.Fatalf("failed to decode image config: %v", err) + } + + const wantFormat = "png" + const wantWidth = 1200 + const wantHeight = 1234 + if format != wantFormat { + t.Fatalf("expected format to be %q, got %q", wantFormat, format) + } + if config.Width != wantWidth || config.Height != wantHeight { + t.Fatalf("expected dimensions to be %d*%d, got %d*%d", + wantWidth, wantHeight, config.Width, config.Height) + } } func TestNavigate(t *testing.T) { - t.Skip("TODO") + t.Parallel() + ctx, cancel := testAllocate(t) + defer cancel() + endpoint := testStartServer(t) + + var got string + tasks := chromedp.Tasks{ + network.Enable(), + EnableLifeCycleEvents(), + Navigate(endpoint+"/navigate.html", 5*time.Second), + chromedp.Text("#text", &got), + } + if err := chromedp.Run(ctx, tasks); err != nil { + t.Fatal(err) + } + const want = "DOMContentLoaded" + if got != want { + t.Fatalf("expected text to be %q, got %q", want, got) + } } func TestIgnoreCacheReload(t *testing.T) { - t.Skip("TODO") -} + t.Parallel() + ctx, cancel := testAllocate(t) + defer cancel() + endpoint := testStartServer(t) -func TestEnableLifeCycleEvents(t *testing.T) { - t.Skip("TODO") + var got string + tasks := chromedp.Tasks{ + network.Enable(), + EnableLifeCycleEvents(), + chromedp.Navigate(endpoint + "/navigate.html"), + IgnoreCacheReload(5 * time.Second), + chromedp.Text("#text", &got), + } + if err := chromedp.Run(ctx, tasks); err != nil { + t.Fatal(err) + } + const want = "DOMContentLoaded" + if got != want { + t.Fatalf("expected text to be %q, got %q", want, got) + } } func TestWaitResponse(t *testing.T) { - t.Skip("TODO") + t.Parallel() + ctx, cancel := testAllocate(t) + defer cancel() + endpoint := testStartServer(t) + + var got string + tasks := chromedp.Tasks{ + network.Enable(), + EnableLifeCycleEvents(), + chromedp.Navigate(endpoint + "/index.html"), + WaitResponse(endpoint+"/navigate.html", 5*time.Second, + chromedp.Click(`a[href="navigate.html"]`), + ), + chromedp.Text("#text", &got), + } + if err := chromedp.Run(ctx, tasks); err != nil { + t.Fatal(err) + } + const want = "DOMContentLoaded" + if got != want { + t.Fatalf("expected text to be %q, got %q", want, got) + } } func TestWaitLoaded(t *testing.T) { - t.Skip("TODO") + t.Parallel() + ctx, cancel := testAllocate(t) + defer cancel() + endpoint := testStartServer(t) + + var got string + tasks := chromedp.Tasks{ + chromedp.Navigate(endpoint + "/index.html"), + chromedp.Click(`a[href="navigate.html"]`), + WaitLoaded(5 * time.Second), + chromedp.Text("#text", &got), + } + if err := chromedp.Run(ctx, tasks); err != nil { + t.Fatal(err) + } + const want = "loaded" + if got != want { + t.Fatalf("expected text to be %q, got %q", want, got) + } } func TestWaitInput(t *testing.T) { diff --git a/testdata/image.png b/testdata/image.png new file mode 100644 index 0000000..8044712 Binary files /dev/null and b/testdata/image.png differ diff --git a/testdata/index.html b/testdata/index.html new file mode 100644 index 0000000..21e52b4 --- /dev/null +++ b/testdata/index.html @@ -0,0 +1,17 @@ + + + + + + + Document for the test + + + +
+
Test
+ navigate +
+ + + \ No newline at end of file diff --git a/testdata/navigate.html b/testdata/navigate.html new file mode 100644 index 0000000..470e1cd --- /dev/null +++ b/testdata/navigate.html @@ -0,0 +1,28 @@ + + + + + + + Document for the test + + + +
+
Test
+
+ not loaded +
+
+ + test image + + + \ No newline at end of file diff --git a/testdata/screenshot.html b/testdata/screenshot.html new file mode 100644 index 0000000..45adbce --- /dev/null +++ b/testdata/screenshot.html @@ -0,0 +1,24 @@ + + + + + + + Document for the test + + + + +
+
Test
+ testtesttest. +
+ + + \ No newline at end of file