diff --git a/element_test.go b/element_test.go
index e10eb7c4..9b163949 100644
--- a/element_test.go
+++ b/element_test.go
@@ -2,6 +2,7 @@ package rod_test
import (
"bytes"
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -10,6 +11,7 @@ import (
"net"
"os"
"path/filepath"
+ "sync"
"testing"
"time"
@@ -709,6 +711,100 @@ func TestWaitStableRAP(t *testing.T) {
g.Err(el.WaitStableRAF())
}
+func TestWaitRepaintHonorsPageContext(t *testing.T) {
+ g := setup(t)
+ ctx, cancel := context.WithCancel(g.page.GetContext())
+ cancel()
+
+ err := g.page.Context(ctx).WaitRepaint()
+ if !errors.Is(err, context.Canceled) {
+ t.Fatalf("WaitRepaint error = %v, want context.Canceled", err)
+ }
+}
+
+const waitStableRAFDemoPage = `
+
+
+`
+
+func TestWaitStableRAFOnBackgroundPageHonorsContext(t *testing.T) {
+ g := setup(t)
+ target := g.newPage(g.html(waitStableRAFDemoPage)).MustWaitLoad()
+ other := g.newPage(g.blank()).MustWaitLoad()
+ other.MustActivate()
+
+ // Freeze the hidden target so requestAnimationFrame cannot resolve. The
+ // operation must still return when its caller context expires.
+ if err := (proto.PageSetWebLifecycleState{State: proto.PageSetWebLifecycleStateStateFrozen}).Call(target); err != nil {
+ t.Fatal(err)
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+ defer cancel()
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- target.MustElement("#target").Context(ctx).WaitStableRAF()
+ }()
+ select {
+ case err := <-errCh:
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("background WaitStableRAF error = %v, want context deadline exceeded", err)
+ }
+ case <-time.After(1 * time.Second):
+ t.Fatal("background WaitStableRAF remained blocked; this reproduces the pre-fix root-context regression")
+ }
+}
+
+func TestWaitStableRAFSurvivesFrequentActivationSwitches(t *testing.T) {
+ g := setup(t)
+ target := g.newPage(g.html(waitStableRAFDemoPage)).MustWaitLoad()
+ other := g.newPage(g.blank()).MustWaitLoad()
+
+ ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
+ defer cancel()
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- target.MustElement("#target").Context(ctx).WaitStableRAF()
+ }()
+
+ done := make(chan struct{})
+ var switchers sync.WaitGroup
+ switchers.Add(1)
+ go func() {
+ defer switchers.Done()
+ ticker := time.NewTicker(10 * time.Millisecond)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ _, _ = target.Activate()
+ _, _ = other.Activate()
+ case <-done:
+ return
+ }
+ }
+ }()
+ defer func() {
+ close(done)
+ switchers.Wait()
+ }()
+
+ select {
+ case err := <-errCh:
+ if !errors.Is(err, context.DeadlineExceeded) {
+ t.Fatalf("WaitStableRAF during activation switches error = %v, want context deadline exceeded", err)
+ }
+ case <-time.After(2 * time.Second):
+ t.Fatal("WaitStableRAF remained blocked during frequent activation switches")
+ }
+}
+
func TestCanvasToImage(t *testing.T) {
g := setup(t)
diff --git a/page.go b/page.go
index f646ecea..0114e4bd 100644
--- a/page.go
+++ b/page.go
@@ -848,7 +848,10 @@ func (p *Page) WaitIdle(timeout time.Duration) (err error) {
// Doc: https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
func (p *Page) WaitRepaint() error {
// we use root here because iframe doesn't trigger requestAnimationFrame
- _, err := p.root.Eval(`() => new Promise(r => requestAnimationFrame(r))`)
+ // but the root evaluation must still inherit the caller's context. Otherwise
+ // Page.Context/Timeout cannot cancel this wait when the root page is hidden
+ // or keeps repainting indefinitely.
+ _, err := p.root.Context(p.ctx).Eval(`() => new Promise(r => requestAnimationFrame(r))`)
return err
}