Skip to content
Merged
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
4 changes: 3 additions & 1 deletion docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,9 @@ work when nobody knows it and that the call site never shows. The mechanism and
none of 30 from 500 ms; Chrome for Testing 155 and Helium 0.19.2.1). The agent hears of a dialog the person
answered, at most twenty between two page answers, but not of one the agent itself answered. A held page cannot lay
out, so a pane opened or resized meanwhile shows it at the size it had, and lays it out at the pane's once the
dialog closes; a screenshot fails until then.
dialog closes; a screenshot fails until then. On Chrome 153 and older, which answers a screencast's start and
stop only once the dialog closes, a pane resized while one is open keeps its last frame until it is answered
(Chrome 151 on the macos-15-intel runner, measured; 154 on answers at once).
A window opened with Open in a window was not tried with a dialog.

## Languages
Expand Down
7 changes: 7 additions & 0 deletions internal/browser/chrome.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"sync"
"time"

"github.com/chromedp/cdproto/browser"
Expand Down Expand Up @@ -87,6 +88,12 @@ type chromePage struct {
dl *downloads
// gate is held while the page has a JavaScript dialog open.
gate dialogGate
// castMu guards castLast, the last screencast start or stop queued, which
// the next one waits for: they can run on past their caller
// (castPastDialog). casting says one is running.
castMu sync.Mutex
castLast chan struct{}
casting bool
}

// downloadFolder is downloadDir, which the live tests point at a folder of
Expand Down
71 changes: 60 additions & 11 deletions internal/browser/chrome_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,22 +190,71 @@ func (p *chromePage) deliver(frame *page.EventScreencastFrame) {
// Screencast starts the frames at v, or restarts them there: Chrome keeps
// the size a screencast started with.
func (p *chromePage) Screencast(ctx context.Context, v Viewport) error {
if err := p.runFree(ctx, page.StopScreencast()); err != nil {
return err
w, h := v.devicePixels()
return p.castPastDialog(func() error {
if err := p.stopCast(ctx); err != nil {
return err
}
if err := p.Resize(ctx, v); err != nil {
return err
}
p.casting = true
return p.runFree(ctx, page.StartScreencast().
WithFormat(page.ScreencastFormatJpeg).
WithQuality(screencastQuality).
WithMaxWidth(w).
WithMaxHeight(h))
})
}

func (p *chromePage) StopScreencast(ctx context.Context) error {
return p.castPastDialog(func() error { return p.stopCast(ctx) })
}

// stopCast stops the frames. With none running it sends nothing, so a pane
// opened on a held page starts its frames without a stop waiting first.
func (p *chromePage) stopCast(ctx context.Context) error {
if !p.casting {
return nil
}
if err := p.Resize(ctx, v); err != nil {
if err := p.runFree(ctx, page.StopScreencast()); err != nil {
return err
}
w, h := v.devicePixels()
return p.runFree(ctx, page.StartScreencast().
WithFormat(page.ScreencastFormatJpeg).
WithQuality(screencastQuality).
WithMaxWidth(w).
WithMaxHeight(h))
p.casting = false
return nil
}

func (p *chromePage) StopScreencast(ctx context.Context) error {
return p.runFree(ctx, page.StopScreencast())
// castPastDialog runs step, a start or stop of the screencast, after the ones
// before it, and returns once it is done or the page holds a dialog. Chrome up
// to 153 answers Page.startScreencast and Page.stopScreencast only once the
// renderer has seen them, and a renderer holding a dialog sees nothing until
// it closes, though the browser starts or stops the frames at once: a pane
// opened on the dialog waited the whole pageWait and lost the browser (Chrome
// 151, macos-15-intel runner, measured 2026-10-28). step runs on after that.
func (p *chromePage) castPastDialog(step func() error) error {
p.castMu.Lock()
prev, mine := p.castLast, make(chan struct{})
p.castLast = mine
p.castMu.Unlock()
done := make(chan error, 1)
go func() {
defer close(mine)
if prev != nil {
<-prev
}
done <- step()
}()
select {
case err := <-done:
return err
case <-p.gate.held():
go func() {
if err := <-done; err != nil && p.Alive() {
slog.Warn("browser: start or stop the screencast under a dialog", "err", err)
}
}()
return nil
}
}

// Resize lays the page out at the pane's size, as a window of that size would.
Expand Down
31 changes: 31 additions & 0 deletions internal/browser/dialogs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,34 @@ func TestTheGateKeepsTheLastSizeAskedForWhileHeld(t *testing.T) {
t.Fatalf("a second close handed back %+v", v)
}
}

// A screencast step stands for Chrome 153 and older here, which answers it
// only once the dialog closes.
func TestAScreencastUnderADialogReturnsAndRunsInOrder(t *testing.T) {
var p chromePage
p.gate.open()
answered := make(chan struct{})
var order []int
first := func() error {
<-answered
order = append(order, 1)
return nil
}
if err := p.castPastDialog(first); err != nil {
t.Fatal(err)
}
ran := make(chan struct{})
second := func() error {
order = append(order, 2)
close(ran)
return nil
}
if err := p.castPastDialog(second); err != nil {
t.Fatal(err)
}
close(answered)
<-ran
if len(order) != 2 || order[0] != 1 {
t.Fatalf("steps ran as %v, want 1 then 2", order)
}
}
Loading