1package chromedp 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "net" 10 "net/http" 11 "net/http/httptest" 12 "os" 13 "path" 14 "strings" 15 "sync" 16 "sync/atomic" 17 "testing" 18 "time" 19 20 "github.com/chromedp/cdproto/browser" 21 "github.com/chromedp/cdproto/cdp" 22 "github.com/chromedp/cdproto/dom" 23 "github.com/chromedp/cdproto/page" 24 cdpruntime "github.com/chromedp/cdproto/runtime" 25 "github.com/chromedp/cdproto/target" 26) 27 28var ( 29 // these are set up in init 30 execPath string 31 testdataDir string 32 allocOpts = DefaultExecAllocatorOptions[:] 33 34 // allocCtx is initialised in TestMain, to cancel before exiting. 35 allocCtx context.Context 36 37 // browserCtx is initialised with allocateOnce 38 browserCtx context.Context 39) 40 41func init() { 42 wd, err := os.Getwd() 43 if err != nil { 44 panic(fmt.Sprintf("could not get working directory: %v", err)) 45 } 46 testdataDir = "file://" + path.Join(wd, "testdata") 47 48 allocTempDir, err = ioutil.TempDir("", "chromedp-test") 49 if err != nil { 50 panic(fmt.Sprintf("could not create temp directory: %v", err)) 51 } 52 53 // Disabling the GPU helps portability with some systems like Travis, 54 // and can slightly speed up the tests on other systems. 55 allocOpts = append(allocOpts, DisableGPU) 56 57 // Find the exec path once at startup. 58 execPath = os.Getenv("CHROMEDP_TEST_RUNNER") 59 if execPath == "" { 60 execPath = findExecPath() 61 } 62 allocOpts = append(allocOpts, ExecPath(execPath)) 63 64 // Not explicitly needed to be set, as this speeds up the tests 65 if noSandbox := os.Getenv("CHROMEDP_NO_SANDBOX"); noSandbox != "false" { 66 allocOpts = append(allocOpts, NoSandbox) 67 } 68} 69 70var browserOpts []ContextOption 71 72func TestMain(m *testing.M) { 73 var cancel context.CancelFunc 74 allocCtx, cancel = NewExecAllocator(context.Background(), allocOpts...) 75 76 if debug := os.Getenv("CHROMEDP_DEBUG"); debug != "" && debug != "false" { 77 browserOpts = append(browserOpts, WithDebugf(log.Printf)) 78 } 79 80 code := m.Run() 81 cancel() 82 83 if infos, _ := ioutil.ReadDir(allocTempDir); len(infos) > 0 { 84 os.RemoveAll(allocTempDir) 85 panic(fmt.Sprintf("leaked %d temporary dirs under %s", len(infos), allocTempDir)) 86 } else { 87 os.Remove(allocTempDir) 88 } 89 90 os.Exit(code) 91} 92 93var allocateOnce sync.Once 94 95func testAllocate(tb testing.TB, name string) (context.Context, context.CancelFunc) { 96 // Start the browser exactly once, as needed. 97 allocateOnce.Do(func() { browserCtx, _ = testAllocateSeparate(tb) }) 98 99 if browserCtx == nil { 100 // allocateOnce.Do failed; continuing would result in panics. 101 tb.FailNow() 102 } 103 104 // Same browser, new tab; not needing to start new chrome browsers for 105 // each test gives a huge speed-up. 106 ctx, _ := NewContext(browserCtx) 107 108 // Only navigate if we want an html file name, otherwise leave the blank page. 109 if name != "" { 110 if err := Run(ctx, Navigate(testdataDir+"/"+name)); err != nil { 111 tb.Fatal(err) 112 } 113 } 114 115 cancel := func() { 116 if err := Cancel(ctx); err != nil { 117 tb.Error(err) 118 } 119 } 120 return ctx, cancel 121} 122 123func testAllocateSeparate(tb testing.TB) (context.Context, context.CancelFunc) { 124 // Entirely new browser, unlike testAllocate. 125 ctx, _ := NewContext(allocCtx, browserOpts...) 126 if err := Run(ctx); err != nil { 127 tb.Fatal(err) 128 } 129 ListenBrowser(ctx, func(ev interface{}) { 130 switch ev := ev.(type) { 131 case *cdpruntime.EventExceptionThrown: 132 tb.Errorf("%+v\n", ev.ExceptionDetails) 133 } 134 }) 135 cancel := func() { 136 if err := Cancel(ctx); err != nil { 137 tb.Error(err) 138 } 139 } 140 return ctx, cancel 141} 142 143func BenchmarkTabNavigate(b *testing.B) { 144 b.ReportAllocs() 145 146 allocCtx, cancel := NewExecAllocator(context.Background(), allocOpts...) 147 defer cancel() 148 149 // start the browser 150 bctx, _ := NewContext(allocCtx) 151 if err := Run(bctx); err != nil { 152 b.Fatal(err) 153 } 154 155 b.RunParallel(func(pb *testing.PB) { 156 for pb.Next() { 157 ctx, _ := NewContext(bctx) 158 if err := Run(ctx, 159 Navigate(testdataDir+"/form.html"), 160 WaitVisible(`#form`, ByID), 161 ); err != nil { 162 b.Fatal(err) 163 } 164 if err := Cancel(ctx); err != nil { 165 b.Fatal(err) 166 } 167 } 168 }) 169} 170 171// checkPages fatals if the browser behind the chromedp context has an 172// unexpected number of pages (tabs). 173func checkTargets(tb testing.TB, ctx context.Context, want int) { 174 tb.Helper() 175 infos, err := Targets(ctx) 176 if err != nil { 177 tb.Fatal(err) 178 } 179 var pages []*target.Info 180 for _, info := range infos { 181 if info.Type == "page" { 182 pages = append(pages, info) 183 } 184 } 185 if got := len(pages); want != got { 186 var summaries []string 187 for _, info := range pages { 188 summaries = append(summaries, fmt.Sprintf("%v", info)) 189 } 190 tb.Fatalf("want %d targets, got %d:\n%s", 191 want, got, strings.Join(summaries, "\n")) 192 } 193} 194 195func TestTargets(t *testing.T) { 196 t.Parallel() 197 198 // Start one browser with one tab. 199 ctx1, cancel1 := testAllocateSeparate(t) 200 defer cancel1() 201 202 checkTargets(t, ctx1, 1) 203 204 // Start a second tab on the same browser. 205 ctx2, cancel2 := NewContext(ctx1) 206 defer cancel2() 207 if err := Run(ctx2); err != nil { 208 t.Fatal(err) 209 } 210 checkTargets(t, ctx2, 2) 211 212 // The first context should also see both targets. 213 checkTargets(t, ctx1, 2) 214 215 // Cancelling the second context should close the second tab alone. 216 cancel2() 217 checkTargets(t, ctx1, 1) 218 219 // We used to have a bug where Run would reset the first context as if 220 // it weren't the first, breaking its cancellation. 221 if err := Run(ctx1); err != nil { 222 t.Fatal(err) 223 } 224 225 // We should see one attached target, since we closed the second a while 226 // ago. If we see two, that means there's a memory leak, as we're 227 // holding onto the detached target. 228 pages := FromContext(ctx1).Browser.pages 229 if len(pages) != 1 { 230 t.Fatalf("expected one attached target, got %d", len(pages)) 231 } 232} 233 234func TestCancelError(t *testing.T) { 235 t.Parallel() 236 237 ctx1, cancel1 := testAllocate(t, "") 238 defer cancel1() 239 if err := Run(ctx1); err != nil { 240 t.Fatal(err) 241 } 242 243 // Open and close a target normally; no error. 244 ctx2, cancel2 := NewContext(ctx1) 245 defer cancel2() 246 if err := Run(ctx2); err != nil { 247 t.Fatal(err) 248 } 249 if err := Cancel(ctx2); err != nil { 250 t.Fatalf("expected a nil error, got %v", err) 251 } 252 253 // Make "cancel" close the wrong target; error. 254 ctx3, cancel3 := NewContext(ctx1) 255 defer cancel3() 256 if err := Run(ctx3); err != nil { 257 t.Fatal(err) 258 } 259 FromContext(ctx3).Target.TargetID = "wrong" 260 if err := Cancel(ctx3); err == nil { 261 t.Fatalf("expected a non-nil error, got %v", err) 262 } 263} 264 265func TestPrematureCancel(t *testing.T) { 266 t.Parallel() 267 268 // Cancel before the browser is allocated. 269 ctx, _ := NewContext(allocCtx, browserOpts...) 270 if err := Cancel(ctx); err != nil { 271 t.Fatal(err) 272 } 273 if err := Run(ctx); err != context.Canceled { 274 t.Fatalf("wanted canceled context error, got %v", err) 275 } 276} 277 278func TestPrematureCancelTab(t *testing.T) { 279 t.Parallel() 280 281 ctx1, cancel := testAllocate(t, "") 282 defer cancel() 283 if err := Run(ctx1); err != nil { 284 t.Fatal(err) 285 } 286 287 ctx2, cancel := NewContext(ctx1) 288 // Cancel after the browser is allocated, but before we've created a new 289 // tab. 290 cancel() 291 if err := Run(ctx2); err != context.Canceled { 292 t.Fatalf("wanted canceled context error, got %v", err) 293 } 294} 295 296func TestPrematureCancelAllocator(t *testing.T) { 297 t.Parallel() 298 299 // To ensure we don't actually fire any Chrome processes. 300 allocCtx, cancel := NewExecAllocator(context.Background(), 301 ExecPath("/do-not-run-chrome")) 302 // Cancel before the browser is allocated. 303 cancel() 304 305 ctx, cancel := NewContext(allocCtx) 306 defer cancel() 307 if err := Run(ctx); err != context.Canceled { 308 t.Fatalf("wanted canceled context error, got %v", err) 309 } 310} 311 312func TestConcurrentCancel(t *testing.T) { 313 t.Parallel() 314 315 // To ensure we don't actually fire any Chrome processes. 316 allocCtx, cancel := NewExecAllocator(context.Background(), 317 ExecPath("/do-not-run-chrome")) 318 defer cancel() 319 320 // 50 is enough for 'go test -race' to easily spot issues. 321 for i := 0; i < 50; i++ { 322 ctx, cancel := NewContext(allocCtx) 323 go cancel() 324 go Run(ctx) 325 } 326} 327 328func TestListenBrowser(t *testing.T) { 329 t.Parallel() 330 331 ctx, cancel := testAllocate(t, "") 332 defer cancel() 333 334 // Check that many ListenBrowser callbacks work, including adding 335 // callbacks after the browser has been allocated. 336 var totalCount int32 337 ListenBrowser(ctx, func(ev interface{}) { 338 // using sync/atomic, as the browser is shared. 339 atomic.AddInt32(&totalCount, 1) 340 }) 341 if err := Run(ctx); err != nil { 342 t.Fatal(err) 343 } 344 seenSessions := make(map[target.SessionID]bool) 345 ListenBrowser(ctx, func(ev interface{}) { 346 if ev, ok := ev.(*target.EventAttachedToTarget); ok { 347 seenSessions[ev.SessionID] = true 348 } 349 }) 350 351 newTabCtx, cancel := NewContext(ctx) 352 defer cancel() 353 if err := Run(newTabCtx, Navigate(testdataDir+"/form.html")); err != nil { 354 t.Fatal(err) 355 } 356 cancel() 357 if id := FromContext(newTabCtx).Target.SessionID; !seenSessions[id] { 358 t.Fatalf("did not see Target.attachedToTarget for %q", id) 359 } 360 if want, got := int32(1), atomic.LoadInt32(&totalCount); got < want { 361 t.Fatalf("want at least %d browser events; got %d", want, got) 362 } 363} 364 365func TestListenTarget(t *testing.T) { 366 t.Parallel() 367 368 ctx, cancel := testAllocate(t, "") 369 defer cancel() 370 371 // Check that many listen callbacks work, including adding callbacks 372 // after the target has been attached to. 373 var navigatedCount, updatedCount int 374 ListenTarget(ctx, func(ev interface{}) { 375 if _, ok := ev.(*page.EventFrameNavigated); ok { 376 navigatedCount++ 377 } 378 }) 379 if err := Run(ctx); err != nil { 380 t.Fatal(err) 381 } 382 ListenTarget(ctx, func(ev interface{}) { 383 if _, ok := ev.(*dom.EventDocumentUpdated); ok { 384 updatedCount++ 385 } 386 }) 387 388 if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { 389 t.Fatal(err) 390 } 391 cancel() 392 if want := 1; navigatedCount != want { 393 t.Fatalf("want %d Page.frameNavigated events; got %d", want, navigatedCount) 394 } 395 if want := 1; updatedCount < want { 396 t.Fatalf("want at least %d DOM.documentUpdated events; got %d", want, updatedCount) 397 } 398} 399 400func TestLargeEventCount(t *testing.T) { 401 t.Parallel() 402 403 ctx, cancel := testAllocate(t, "") 404 defer cancel() 405 406 // Simulate an environment where Chrome sends 2000 console log events, 407 // and we are slow at processing them. In older chromedp versions, this 408 // would crash as we would fill eventQueue and panic. 50ms is enough to 409 // make the test fail somewhat reliably on old chromedp versions, 410 // without making the test too slow. 411 first := true 412 ListenTarget(ctx, func(ev interface{}) { 413 if _, ok := ev.(*cdpruntime.EventConsoleAPICalled); ok && first { 414 time.Sleep(50 * time.Millisecond) 415 first = false 416 } 417 }) 418 419 if err := Run(ctx, 420 Navigate(testdataDir+"/consolespam.html"), 421 WaitVisible("#done", ByID), // wait for the JS to finish 422 ); err != nil { 423 t.Fatal(err) 424 } 425} 426 427func TestLargeQuery(t *testing.T) { 428 t.Parallel() 429 430 ctx, cancel := testAllocate(t, "") 431 defer cancel() 432 433 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 434 fmt.Fprintf(w, "<html><body>\n") 435 for i := 0; i < 2000; i++ { 436 fmt.Fprintf(w, `<div>`) 437 fmt.Fprintf(w, `<a href="/%d">link %d</a>`, i, i) 438 fmt.Fprintf(w, `</div>`) 439 } 440 fmt.Fprintf(w, "</body></html>\n") 441 })) 442 defer s.Close() 443 444 // ByQueryAll queries thousands of events, which triggers thousands of 445 // DOM events. The target handler used to get into a deadlock, as the 446 // event queues would fill up and prevent the wait function from 447 // receiving any result. 448 var nodes []*cdp.Node 449 if err := Run(ctx, 450 Navigate(s.URL), 451 Nodes("a", &nodes, ByQueryAll), 452 ); err != nil { 453 t.Fatal(err) 454 } 455} 456 457func TestDialTimeout(t *testing.T) { 458 t.Parallel() 459 460 t.Run("ShortTimeoutError", func(t *testing.T) { 461 t.Parallel() 462 l, err := net.Listen("tcp", ":0") 463 if err != nil { 464 t.Fatal(err) 465 } 466 url := "ws://" + l.(*net.TCPListener).Addr().String() 467 defer l.Close() 468 469 ctx, cancel := context.WithCancel(context.Background()) 470 defer cancel() 471 _, err = NewBrowser(ctx, url, WithDialTimeout(time.Microsecond)) 472 got, want := fmt.Sprintf("%v", err), "i/o timeout" 473 if !strings.Contains(got, want) { 474 t.Fatalf("got %q, want %q", got, want) 475 } 476 }) 477 t.Run("NoTimeoutSuccess", func(t *testing.T) { 478 t.Parallel() 479 l, err := net.Listen("tcp", ":0") 480 if err != nil { 481 t.Fatal(err) 482 } 483 url := "ws://" + l.(*net.TCPListener).Addr().String() 484 defer l.Close() 485 go func() { 486 conn, err := l.Accept() 487 if err == nil { 488 conn.Close() 489 } 490 }() 491 492 ctx, cancel := context.WithCancel(context.Background()) 493 defer cancel() 494 _, err = NewBrowser(ctx, url, WithDialTimeout(0)) 495 got := fmt.Sprintf("%v", err) 496 if !strings.Contains(got, "EOF") && !strings.Contains(got, "connection reset") { 497 t.Fatalf("got %q, want %q or %q", got, "EOF", "connection reset") 498 } 499 }) 500} 501 502func TestListenCancel(t *testing.T) { 503 t.Parallel() 504 505 ctx, cancel := testAllocateSeparate(t) 506 defer cancel() 507 508 // Check that cancelling a listen context stops the listener. 509 var browserCount, targetCount int 510 511 ctx1, cancel1 := context.WithCancel(ctx) 512 ListenBrowser(ctx1, func(ev interface{}) { 513 browserCount++ 514 cancel1() 515 }) 516 517 ctx2, cancel2 := context.WithCancel(ctx) 518 ListenTarget(ctx2, func(ev interface{}) { 519 targetCount++ 520 cancel2() 521 }) 522 523 if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { 524 t.Fatal(err) 525 } 526 if want := 1; browserCount != 1 { 527 t.Fatalf("want %d browser events; got %d", want, browserCount) 528 } 529 if want := 1; targetCount != 1 { 530 t.Fatalf("want %d target events; got %d", want, targetCount) 531 } 532} 533 534func TestLogOptions(t *testing.T) { 535 t.Parallel() 536 537 var bufMu sync.Mutex 538 var buf bytes.Buffer 539 fn := func(format string, a ...interface{}) { 540 bufMu.Lock() 541 fmt.Fprintf(&buf, format, a...) 542 fmt.Fprintln(&buf) 543 bufMu.Unlock() 544 } 545 546 ctx, cancel := NewContext(context.Background(), 547 WithErrorf(fn), 548 WithLogf(fn), 549 WithDebugf(fn), 550 ) 551 defer cancel() 552 if err := Run(ctx, Navigate(testdataDir+"/form.html")); err != nil { 553 t.Fatal(err) 554 } 555 cancel() 556 557 bufMu.Lock() 558 got := buf.String() 559 bufMu.Unlock() 560 for _, want := range []string{ 561 "Page.navigate", 562 "Page.frameNavigated", 563 } { 564 if !strings.Contains(got, want) { 565 t.Errorf("expected output to contain %q", want) 566 } 567 } 568} 569 570func TestLargeOutboundMessages(t *testing.T) { 571 t.Parallel() 572 573 ctx, cancel := testAllocate(t, "") 574 defer cancel() 575 576 // ~50KiB of JS should fit just fine in our current buffer of 1MiB. 577 expr := fmt.Sprintf("//%s\n", strings.Repeat("x", 50<<10)) 578 res := new([]byte) 579 if err := Run(ctx, Evaluate(expr, res)); err != nil { 580 t.Fatal(err) 581 } 582} 583 584func TestDirectCloseTarget(t *testing.T) { 585 t.Parallel() 586 587 ctx, cancel := testAllocate(t, "") 588 defer cancel() 589 590 c := FromContext(ctx) 591 want := "to close the target, cancel its context" 592 593 // Check that nothing is closed by running the action twice. 594 for i := 0; i < 2; i++ { 595 err := Run(ctx, ActionFunc(func(ctx context.Context) error { 596 _, err := target.CloseTarget(c.Target.TargetID).Do(ctx) 597 if err != nil { 598 return err 599 } 600 return nil 601 })) 602 got := fmt.Sprint(err) 603 if !strings.Contains(got, want) { 604 t.Fatalf("want %q, got %q", want, got) 605 } 606 } 607} 608 609func TestDirectCloseBrowser(t *testing.T) { 610 t.Parallel() 611 612 ctx, cancel := testAllocateSeparate(t) 613 defer cancel() 614 615 c := FromContext(ctx) 616 want := "use chromedp.Cancel" 617 618 // Check that nothing is closed by running the action twice. 619 for i := 0; i < 2; i++ { 620 err := browser.Close().Do(cdp.WithExecutor(ctx, c.Browser)) 621 got := fmt.Sprint(err) 622 if !strings.Contains(got, want) { 623 t.Fatalf("want %q, got %q", want, got) 624 } 625 } 626} 627 628func TestDownloadIntoDir(t *testing.T) { 629 t.Parallel() 630 631 ctx, cancel := testAllocate(t, "") 632 defer cancel() 633 634 dir, err := ioutil.TempDir("", "chromedp-test") 635 if err != nil { 636 t.Fatal(err) 637 } 638 defer os.RemoveAll(dir) 639 640 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 641 switch r.URL.Path { 642 case "/data.bin": 643 w.Header().Set("Content-Type", "application/octet-stream") 644 fmt.Fprintf(w, "some binary data") 645 default: 646 w.Header().Set("Content-Type", "text/html") 647 fmt.Fprintf(w, `go <a id="download" href="/data.bin">download</a> stuff/`) 648 } 649 })) 650 defer s.Close() 651 652 if err := Run(ctx, 653 Navigate(s.URL), 654 page.SetDownloadBehavior(page.SetDownloadBehaviorBehaviorAllow).WithDownloadPath(dir), 655 Click("#download", ByQuery), 656 ); err != nil { 657 t.Fatal(err) 658 } 659 660 // TODO: wait for the download to finish, and check that the file is in 661 // the directory. 662} 663 664func TestGracefulBrowserShutdown(t *testing.T) { 665 t.Parallel() 666 667 dir, err := ioutil.TempDir("", "chromedp-test") 668 if err != nil { 669 log.Fatal(err) 670 } 671 defer os.RemoveAll(dir) 672 673 // TODO(mvdan): this doesn't work with DefaultExecAllocatorOptions+UserDataDir 674 opts := []ExecAllocatorOption{ 675 NoFirstRun, 676 NoDefaultBrowserCheck, 677 Headless, 678 UserDataDir(dir), 679 } 680 actx, cancel := NewExecAllocator(context.Background(), opts...) 681 defer cancel() 682 683 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 684 if r.RequestURI == "/set" { 685 http.SetCookie(w, &http.Cookie{ 686 Name: "cookie1", 687 Value: "value1", 688 Expires: time.Now().AddDate(0, 0, 1), // one day later 689 }) 690 } 691 })) 692 defer ts.Close() 693 694 { 695 ctx, _ := NewContext(actx) 696 if err := Run(ctx, Navigate(ts.URL+"/set")); err != nil { 697 t.Fatal(err) 698 } 699 700 // Close the browser gracefully. 701 if err := Cancel(ctx); err != nil { 702 t.Fatal(err) 703 } 704 } 705 { 706 ctx, _ := NewContext(actx) 707 var got string 708 if err := Run(ctx, 709 Navigate(ts.URL), 710 EvaluateAsDevTools("document.cookie", &got), 711 ); err != nil { 712 t.Fatal(err) 713 } 714 if want := "cookie1=value1"; got != want { 715 t.Fatalf("want cookies %q; got %q", want, got) 716 } 717 } 718} 719