Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions server/lib/cdpclient/cdpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,10 +592,38 @@ func relatedHosts(a, b string) bool {
return a == b || strings.HasSuffix(a, "."+b) || strings.HasSuffix(b, "."+a)
}

// DevTools can accept connections shortly before Chromium opens its first tab,
// so firstPageTargetID polls for a page target. Vars so tests can adjust them.
var (
pageTargetWaitTimeout = 5 * time.Second
pageTargetPollInterval = 100 * time.Millisecond
)

// firstPageTargetID returns the targetId of the first page target reported
// by Target.getTargets. Callers that need to operate on the user-facing
// browser window (Emulation, Browser.* window bounds) use this to find it.
// by Target.getTargets, polling for up to pageTargetWaitTimeout if none
// exists yet. Callers that need to operate on the user-facing browser window
// (Emulation, Browser.* window bounds) use this to find it.
func (c *Client) firstPageTargetID(ctx context.Context) (string, error) {
deadline := time.Now().Add(pageTargetWaitTimeout)
for {
targetID, err := c.findPageTargetID(ctx)
if err != nil || targetID != "" {
return targetID, err
}
if time.Now().After(deadline) {
return "", fmt.Errorf("no page target found")
}
select {
case <-ctx.Done():
return "", fmt.Errorf("no page target found: %w", ctx.Err())
case <-time.After(min(pageTargetPollInterval, time.Until(deadline))):
}
}
}

// findPageTargetID returns the targetId of the first page target, or "" if
// there is none.
func (c *Client) findPageTargetID(ctx context.Context) (string, error) {
targetsResult, err := c.Send(ctx, "Target.getTargets", nil, "")
if err != nil {
return "", fmt.Errorf("Target.getTargets: %w", err)
Expand All @@ -614,7 +642,7 @@ func (c *Client) firstPageTargetID(ctx context.Context) (string, error) {
return t.TargetID, nil
}
}
return "", fmt.Errorf("no page target found")
return "", nil
}

// SetWindowBoundsMaximized puts the OS window backing the first page target
Expand Down
65 changes: 60 additions & 5 deletions server/lib/cdpclient/cdpclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
// fakeCDP is a minimal CDP server that responds to the commands used by
// SetDeviceMetricsOverride and GetBrowserVersion.
type fakeCDP struct {
getTargetsCalled bool
getTargetsCalls int
attachCalled bool
setMetricsCalled bool
setMetricsWidth int
Expand All @@ -29,6 +29,7 @@ type fakeCDP struct {
failGetTargets bool
failSetMetrics bool
returnNoPageTargets bool
noPageTargetsFor int
getVersionCalled bool
failGetVersion bool
productResponse string
Expand Down Expand Up @@ -70,12 +71,12 @@ func (f *fakeCDP) handler(w http.ResponseWriter, r *http.Request) {

switch req.Method {
case "Target.getTargets":
f.getTargetsCalled = true
f.getTargetsCalls++
if f.failGetTargets {
cdpErr = &Error{Code: -1, Message: "mock error"}
} else {
targets := []map[string]string{}
if !f.returnNoPageTargets {
if !f.returnNoPageTargets && f.getTargetsCalls > f.noPageTargetsFor {
targets = append(targets, map[string]string{
"targetId": f.pageTargetID,
"type": "page",
Expand Down Expand Up @@ -187,7 +188,7 @@ func TestSetDeviceMetricsOverride(t *testing.T) {
err = client.SetDeviceMetricsOverride(ctx, 1920, 1080)
require.NoError(t, err)

assert.True(t, f.getTargetsCalled)
assert.Equal(t, 1, f.getTargetsCalls)
assert.True(t, f.attachCalled)
assert.True(t, f.setMetricsCalled)
assert.True(t, f.detachCalled)
Expand All @@ -196,19 +197,73 @@ func TestSetDeviceMetricsOverride(t *testing.T) {
})

t.Run("no page target", func(t *testing.T) {
defer func(d time.Duration) { pageTargetWaitTimeout = d }(pageTargetWaitTimeout)
pageTargetWaitTimeout = 300 * time.Millisecond
defer func(d time.Duration) { pageTargetPollInterval = d }(pageTargetPollInterval)
pageTargetPollInterval = time.Minute // only the deadline clamp ends the wait in time

f := &fakeCDP{
returnNoPageTargets: true,
}
url := startFakeCDP(t, f)

ctx := context.Background()
// Fail instead of hanging if the wait is unbounded.
ctx, cancel := context.WithTimeout(context.Background(), pageTargetWaitTimeout+time.Second)
defer cancel()
client, err := Dial(ctx, url)
require.NoError(t, err)
defer client.Close()

start := time.Now()
err = client.SetDeviceMetricsOverride(ctx, 1920, 1080)
require.Error(t, err)
assert.Contains(t, err.Error(), "no page target found")
assert.NotErrorIs(t, err, context.DeadlineExceeded)
assert.GreaterOrEqual(t, time.Since(start), pageTargetWaitTimeout)
assert.False(t, f.attachCalled)
})

t.Run("waits for first page target", func(t *testing.T) {
f := &fakeCDP{
pageTargetID: "target-123",
sessionID: "session-abc",
noPageTargetsFor: 3,
}
url := startFakeCDP(t, f)

ctx := context.Background()
client, err := Dial(ctx, url)
require.NoError(t, err)
defer client.Close()

err = client.SetDeviceMetricsOverride(ctx, 1920, 1080)
require.NoError(t, err)

assert.Equal(t, 4, f.getTargetsCalls)
assert.True(t, f.setMetricsCalled)
})

t.Run("no page target respects context", func(t *testing.T) {
defer func(d time.Duration) { pageTargetPollInterval = d }(pageTargetPollInterval)
pageTargetPollInterval = time.Minute // only ctx.Done() can end the wait early

f := &fakeCDP{
returnNoPageTargets: true,
}
url := startFakeCDP(t, f)

client, err := Dial(context.Background(), url)
require.NoError(t, err)
defer client.Close()

ctx, cancel := context.WithCancel(context.Background())
time.AfterFunc(250*time.Millisecond, cancel)

start := time.Now()
err = client.SetDeviceMetricsOverride(ctx, 1920, 1080)
require.Error(t, err)
assert.ErrorIs(t, err, context.Canceled)
assert.Less(t, time.Since(start), pageTargetWaitTimeout)
})

t.Run("getTargets failure", func(t *testing.T) {
Expand Down
Loading