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