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 + + + + + + + + +
+ loading +
+ + + + + + + + + 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)