diff --git a/cspell.json b/cspell.json
index 1cb58cf6..2d990f73 100644
--- a/cspell.json
+++ b/cspell.json
@@ -114,7 +114,8 @@
"wsutil",
"xlink",
"XVFB",
- "ysmood"
+ "ysmood",
+ "Rects"
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
diff --git a/fixtures/page-wait-stable.html b/fixtures/page-wait-stable.html
new file mode 100644
index 00000000..b36a2172
--- /dev/null
+++ b/fixtures/page-wait-stable.html
@@ -0,0 +1,90 @@
+
+
+
+ PageWaitStable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/must.go b/must.go
index 96950c9e..7bd11e0d 100644
--- a/must.go
+++ b/must.go
@@ -365,6 +365,13 @@ func (p *Page) MustScreenshot(toFile ...string) []byte {
return bin
}
+// MustCaptureDOMSnapshot is similar to CaptureDOMSnapshot.
+func (p *Page) MustCaptureDOMSnapshot() (domSnapshot *proto.DOMSnapshotCaptureSnapshotResult) {
+ domSnapshot, err := p.CaptureDOMSnapshot()
+ p.e(err)
+ return domSnapshot
+}
+
// MustScreenshotFullPage is similar to ScreenshotFullPage.
// If the toFile is "", it Page.will save output to "tmp/screenshots" folder, time as the file name.
func (p *Page) MustScreenshotFullPage(toFile ...string) []byte {
@@ -412,6 +419,12 @@ func (p *Page) MustWaitIdle() *Page {
return p
}
+// MustWaitStable is similar to Page.WaitStable
+func (p *Page) MustWaitStable() *Page {
+ p.e(p.WaitStable(800*time.Millisecond, 1))
+ return p
+}
+
// MustWaitLoad is similar to Page.WaitLoad
func (p *Page) MustWaitLoad() *Page {
p.e(p.WaitLoad())
diff --git a/page.go b/page.go
index a73c766f..fece64fd 100644
--- a/page.go
+++ b/page.go
@@ -16,6 +16,7 @@ import (
"github.com/go-rod/rod/lib/proto"
"github.com/go-rod/rod/lib/utils"
"github.com/ysmood/goob"
+ "github.com/ysmood/got/lib/lcs"
"github.com/ysmood/gson"
)
@@ -442,6 +443,30 @@ func (p *Page) Screenshot(fullPage bool, req *proto.PageCaptureScreenshot) ([]by
return shot.Data, nil
}
+// CaptureDOMSnapshot Returns a document snapshot, including the full DOM tree of the root node
+// (including iframes, template contents, and imported documents) in a flattened array,
+// as well as layout and white-listed computed style information for the nodes.
+// Shadow DOM in the returned DOM tree is flattened.
+// `Documents` The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.
+// `Strings` Shared string table that all string properties refer to with indexes.
+// Normally use `Strings` is enough.
+func (p *Page) CaptureDOMSnapshot() (domSnapshot *proto.DOMSnapshotCaptureSnapshotResult, err error) {
+ _ = proto.DOMSnapshotEnable{}.Call(p)
+
+ snapshot, err := proto.DOMSnapshotCaptureSnapshot{
+ ComputedStyles: []string{},
+ IncludePaintOrder: true,
+ IncludeDOMRects: true,
+ IncludeBlendedBackgroundColors: true,
+ IncludeTextColorOpacities: true,
+ }.Call(p)
+
+ if err != nil {
+ return nil, err
+ }
+ return snapshot, nil
+}
+
// PDF prints page as PDF
func (p *Page) PDF(req *proto.PagePrintToPDF) (*StreamReader, error) {
req.TransferMode = proto.PagePrintToPDFTransferModeReturnAsStream
@@ -584,6 +609,53 @@ func (p *Page) WaitRequestIdle(d time.Duration, includes, excludes []string) fun
}
}
+// WaitStable like "Element.WaitStable". WaitStable polling the changes
+// of the DOM tree in `d` duration,until the similarity equal or more than simThreshold.
+// `simThreshold` is the similarity threshold,it's scope in [0,1].
+// Be careful,d is not the max wait timeout, it's the least stable time.
+// If you want to set a timeout you can use the "Page.Timeout" function.
+func (p *Page) WaitStable(d time.Duration, similarity float32) error {
+ err := p.WaitLoad()
+ if err != nil {
+ return err
+ }
+
+ defer p.tryTrace(TraceTypeWait, "stable")
+
+ domSnapshot, err := p.CaptureDOMSnapshot()
+ if err != nil {
+ return err
+ }
+
+ t := time.NewTicker(d)
+ defer t.Stop()
+
+ for {
+ select {
+ case <-t.C:
+ case <-p.ctx.Done():
+ return p.ctx.Err()
+ }
+
+ currentDomSnapshot, err := p.CaptureDOMSnapshot()
+ if err != nil {
+ return err
+ }
+
+ xs := lcs.NewWords(domSnapshot.Strings)
+ ys := lcs.NewWords(currentDomSnapshot.Strings)
+ diff := xs.YadLCS(p.ctx, ys)
+
+ sim := float32(len(diff)) / float32(len(ys))
+ if sim >= similarity {
+ break
+ }
+
+ domSnapshot = currentDomSnapshot
+ }
+ return nil
+}
+
// WaitIdle waits until the next window.requestIdleCallback is called.
func (p *Page) WaitIdle(timeout time.Duration) (err error) {
_, err = p.Evaluate(evalHelper(js.WaitIdle, timeout.Milliseconds()).ByPromise())
diff --git a/page_test.go b/page_test.go
index 84ef3e18..5628c61c 100644
--- a/page_test.go
+++ b/page_test.go
@@ -506,6 +506,56 @@ func TestPageWaitRequestIdle(t *testing.T) {
})
}
+func TestPageCaptureDOMSnapshot(t *testing.T) {
+ g := setup(t)
+
+ p := g.page.MustNavigate(g.srcFile("fixtures/click.html"))
+ domSnapshot := p.MustCaptureDOMSnapshot()
+ g.Is(domSnapshot.Strings, []string{})
+
+ timeOutPage := p.Timeout(1 * time.Second)
+ utils.Sleep(1)
+ snapshot, err := timeOutPage.CaptureDOMSnapshot()
+ g.Is(err, context.DeadlineExceeded)
+ g.Nil(snapshot)
+
+}
+
+func TestPageWaitStable(t *testing.T) {
+ g := setup(t)
+
+ // for waitLoad failed
+ g.Panic(func() {
+ g.mc.stubErr(1, proto.RuntimeCallFunctionOn{})
+ g.page.MustWaitStable()
+ })
+
+ p := g.page.MustNavigate(g.srcFile("fixtures/page-wait-stable.html"))
+ // wait for p loading and rending complete
+ p.MustWaitStable()
+
+ // for waitStable timeout
+ timeOutPage := p.Timeout(1 * time.Second)
+ err := timeOutPage.WaitStable(2*time.Second, 1)
+ g.Is(err, context.DeadlineExceeded)
+
+ {
+ g.Panic(func() {
+ p := g.page.MustNavigate(g.srcFile("fixtures/page-wait-stable.html"))
+ g.mc.stubErr(1, proto.DOMSnapshotCaptureSnapshot{})
+ p.MustWaitStable()
+ })
+ }
+
+ {
+ g.Panic(func() {
+ p := g.page.MustNavigate(g.srcFile("fixtures/page-wait-stable.html"))
+ g.mc.stubErr(2, proto.DOMSnapshotCaptureSnapshot{})
+ p.MustWaitStable()
+ })
+ }
+}
+
func TestPageWaitIdle(t *testing.T) {
g := setup(t)