Files
phishingclub/backend/vendor/github.com/go-rod/rod/page.go
T
Ronni Skansing f5e6f53d75 update vendor
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2026-05-24 09:57:07 +02:00

1062 lines
28 KiB
Go

package rod
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
"github.com/go-rod/rod/lib/cdp"
"github.com/go-rod/rod/lib/devices"
"github.com/go-rod/rod/lib/js"
"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"
)
// Page implements these interfaces.
var (
_ proto.Client = &Page{}
_ proto.Contextable = &Page{}
_ proto.Sessionable = &Page{}
)
// Page represents the webpage.
// We try to hold as less states as possible.
// When a page is closed by Rod or not all the ongoing operations an events on it will abort.
type Page struct {
// TargetID is a unique ID for a remote page.
// It's usually used in events sent from the browser to tell which page an event belongs to.
TargetID proto.TargetTargetID
// FrameID is a unique ID for a browsing context.
// Usually, different FrameID means different javascript execution context.
// Such as an iframe and the page it belongs to will have the same TargetID but different FrameIDs.
FrameID proto.PageFrameID
// SessionID is a unique ID for a page attachment to a controller.
// It's usually used in transport layer to tell which page to send the control signal.
// A page can attached to multiple controllers, the browser uses it distinguish controllers.
SessionID proto.TargetSessionID
e eFunc
ctx context.Context
// Used to abort all ongoing actions when a page closes.
sessionCancel func()
root *Page
sleeper func() utils.Sleeper
browser *Browser
event *goob.Observable
// devices
Mouse *Mouse
Keyboard *Keyboard
Touch *Touch
element *Element // iframe only
jsCtxLock *sync.Mutex
jsCtxID *proto.RuntimeRemoteObjectID // use pointer so that page clones can share the change
helpersLock *sync.Mutex
helpers map[proto.RuntimeRemoteObjectID]map[string]proto.RuntimeRemoteObjectID
}
// String interface.
func (p *Page) String() string {
id := p.TargetID
if len(id) > 8 {
id = id[:8]
}
return fmt.Sprintf("<page:%s>", id)
}
// IsIframe tells if it's iframe.
func (p *Page) IsIframe() bool {
return p.element != nil
}
// GetSessionID interface.
func (p *Page) GetSessionID() proto.TargetSessionID {
return p.SessionID
}
// Browser of the page.
func (p *Page) Browser() *Browser {
return p.browser
}
// Info of the page, such as the URL or title of the page.
func (p *Page) Info() (*proto.TargetTargetInfo, error) {
return p.browser.pageInfo(p.TargetID)
}
// HTML of the page.
func (p *Page) HTML() (string, error) {
el, err := p.Element("html")
if err != nil {
return "", err
}
return el.HTML()
}
// Cookies returns the page cookies. By default it will return the cookies for current page.
// The urls is the list of URLs for which applicable cookies will be fetched.
func (p *Page) Cookies(urls []string) ([]*proto.NetworkCookie, error) {
if len(urls) == 0 {
info, err := p.Info()
if err != nil {
return nil, err
}
urls = []string{info.URL}
}
res, err := proto.NetworkGetCookies{Urls: urls}.Call(p)
if err != nil {
return nil, err
}
return res.Cookies, nil
}
// SetCookies is similar to Browser.SetCookies .
func (p *Page) SetCookies(cookies []*proto.NetworkCookieParam) error {
if cookies == nil {
return proto.NetworkClearBrowserCookies{}.Call(p)
}
return proto.NetworkSetCookies{Cookies: cookies}.Call(p)
}
// SetExtraHeaders whether to always send extra HTTP headers with the requests from this page.
func (p *Page) SetExtraHeaders(dict []string) (func(), error) {
headers := proto.NetworkHeaders{}
for i := 0; i < len(dict); i += 2 {
headers[dict[i]] = gson.New(dict[i+1])
}
return p.EnableDomain(&proto.NetworkEnable{}), proto.NetworkSetExtraHTTPHeaders{Headers: headers}.Call(p)
}
// SetUserAgent (browser brand, accept-language, etc) of the page.
// If req is nil, a default user agent will be used, a typical mac chrome.
func (p *Page) SetUserAgent(req *proto.NetworkSetUserAgentOverride) error {
if req == nil {
req = devices.LaptopWithMDPIScreen.UserAgentEmulation()
}
return req.Call(p)
}
// SetBlockedURLs For some requests that do not want to be triggered,
// such as some dangerous operations, delete, quit logout, etc.
// Wildcards ('*') are allowed, such as ["*/api/logout/*","delete"].
// NOTE: if you set empty pattern "", it will block all requests.
func (p *Page) SetBlockedURLs(urls []string) error {
if len(urls) == 0 {
return nil
}
return proto.NetworkSetBlockedURLs{Urls: urls}.Call(p)
}
// Navigate to the url. If the url is empty, "about:blank" will be used.
// It will return immediately after the server responds the http header.
func (p *Page) Navigate(url string) error {
if url == "" {
url = "about:blank"
}
// try to stop loading
_ = p.StopLoading()
res, err := proto.PageNavigate{URL: url}.Call(p)
if err != nil {
return err
}
if res.ErrorText != "" {
return &NavigationError{res.ErrorText}
}
p.root.unsetJSCtxID()
return nil
}
// NavigateBack history.
func (p *Page) NavigateBack() error {
// Not using cdp API because it doesn't work for iframe
_, err := p.Evaluate(Eval(`() => history.back()`).ByUser())
return err
}
// ResetNavigationHistory reset history.
func (p *Page) ResetNavigationHistory() error {
err := proto.PageResetNavigationHistory{}.Call(p)
return err
}
// GetNavigationHistory get navigation history.
func (p *Page) GetNavigationHistory() (*proto.PageGetNavigationHistoryResult, error) {
return proto.PageGetNavigationHistory{}.Call(p)
}
// NavigateForward history.
func (p *Page) NavigateForward() error {
// Not using cdp API because it doesn't work for iframe
_, err := p.Evaluate(Eval(`() => history.forward()`).ByUser())
return err
}
// Reload page.
func (p *Page) Reload() error {
p, cancel := p.WithCancel()
defer cancel()
wait := p.EachEvent(func(e *proto.PageFrameNavigated) bool {
return e.Frame.ID == p.FrameID
})
// Not using cdp API because it doesn't work for iframe
_, err := p.Evaluate(Eval(`() => location.reload()`).ByUser())
if err != nil {
return err
}
wait()
p.unsetJSCtxID()
return nil
}
// Activate (focuses) the page.
func (p *Page) Activate() (*Page, error) {
err := proto.TargetActivateTarget{TargetID: p.TargetID}.Call(p.browser)
return p, err
}
func (p *Page) getWindowID() (proto.BrowserWindowID, error) {
res, err := proto.BrowserGetWindowForTarget{TargetID: p.TargetID}.Call(p)
if err != nil {
return 0, err
}
return res.WindowID, err
}
// GetWindow position and size info.
func (p *Page) GetWindow() (*proto.BrowserBounds, error) {
id, err := p.getWindowID()
if err != nil {
return nil, err
}
res, err := proto.BrowserGetWindowBounds{WindowID: id}.Call(p)
if err != nil {
return nil, err
}
return res.Bounds, nil
}
// SetWindow location and size.
func (p *Page) SetWindow(bounds *proto.BrowserBounds) error {
id, err := p.getWindowID()
if err != nil {
return err
}
err = proto.BrowserSetWindowBounds{WindowID: id, Bounds: bounds}.Call(p)
return err
}
// SetViewport overrides the values of device screen dimensions.
func (p *Page) SetViewport(params *proto.EmulationSetDeviceMetricsOverride) error {
if params == nil {
return proto.EmulationClearDeviceMetricsOverride{}.Call(p)
}
return params.Call(p)
}
// SetDocumentContent sets the page document html content.
func (p *Page) SetDocumentContent(html string) error {
return proto.PageSetDocumentContent{
FrameID: p.FrameID,
HTML: html,
}.Call(p)
}
// Emulate the device, such as iPhone9. If device is devices.Clear, it will clear the override.
func (p *Page) Emulate(device devices.Device) error {
err := p.SetViewport(device.MetricsEmulation())
if err != nil {
return err
}
err = device.TouchEmulation().Call(p)
if err != nil {
return err
}
return p.SetUserAgent(device.UserAgentEmulation())
}
// StopLoading forces the page stop navigation and pending resource fetches.
func (p *Page) StopLoading() error {
return proto.PageStopLoading{}.Call(p)
}
// Close tries to close page, running its beforeunload hooks, if has any.
func (p *Page) Close() error {
p.browser.targetsLock.Lock()
defer p.browser.targetsLock.Unlock()
success := true
ctx, cancel := context.WithCancel(p.ctx)
defer cancel()
messages := p.browser.Context(ctx).Event()
for {
err := proto.PageClose{}.Call(p)
if errors.Is(err, cdp.ErrNotAttachedToActivePage) {
// TODO: I don't know why chromium doesn't allow us to close a page while it's navigating.
// Looks like a bug in chromium.
utils.Sleep(0.1)
continue
} else if err != nil {
return err
}
break
}
for msg := range messages {
stop := false
destroyed := proto.TargetTargetDestroyed{}
closed := proto.PageJavascriptDialogClosed{}
if msg.Load(&destroyed) {
stop = destroyed.TargetID == p.TargetID
} else if msg.SessionID == p.SessionID && msg.Load(&closed) {
success = closed.Result
stop = !success
}
if stop {
break
}
}
if success {
p.cleanupStates()
} else {
return &PageCloseCanceledError{}
}
return nil
}
// TriggerFavicon supports when browser in headless mode
// to trigger favicon's request. Pay attention to this
// function only supported when browser in headless mode,
// if you call it in no-headless mode, it will raise an error
// with the message "browser is no-headless".
func (p *Page) TriggerFavicon() error {
// check if browser whether in headless mode
// if not in headless mode then raise error
if !p.browser.isHeadless() {
return errors.New("browser is no-headless")
}
_, err := p.Evaluate(evalHelper(js.TriggerFavicon).ByPromise())
if err != nil {
return err
}
return nil
}
// HandleDialog accepts or dismisses next JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).
// Because modal dialog will block js, usually you have to trigger the dialog in another goroutine.
// For example:
//
// wait, handle := page.MustHandleDialog()
// go page.MustElement("button").MustClick()
// wait()
// handle(true, "")
func (p *Page) HandleDialog() (
wait func() *proto.PageJavascriptDialogOpening,
handle func(*proto.PageHandleJavaScriptDialog) error,
) {
restore := p.EnableDomain(&proto.PageEnable{})
var e proto.PageJavascriptDialogOpening
w := p.WaitEvent(&e)
return func() *proto.PageJavascriptDialogOpening {
w()
return &e
}, func(h *proto.PageHandleJavaScriptDialog) error {
defer restore()
return h.Call(p)
}
}
// HandleFileDialog return a functions that waits for the next file chooser dialog pops up and returns the element
// for the event.
func (p *Page) HandleFileDialog() (func([]string) error, error) {
err := proto.PageSetInterceptFileChooserDialog{Enabled: true}.Call(p)
if err != nil {
return nil, err
}
var e proto.PageFileChooserOpened
w := p.WaitEvent(&e)
return func(paths []string) error {
w()
err := proto.PageSetInterceptFileChooserDialog{Enabled: false}.Call(p)
if err != nil {
return err
}
return proto.DOMSetFileInputFiles{
Files: utils.AbsolutePaths(paths),
BackendNodeID: e.BackendNodeID,
}.Call(p)
}, nil
}
// Screenshot captures the screenshot of current page.
func (p *Page) Screenshot(fullPage bool, req *proto.PageCaptureScreenshot) ([]byte, error) {
if req == nil {
req = &proto.PageCaptureScreenshot{}
}
if fullPage {
metrics, err := proto.PageGetLayoutMetrics{}.Call(p)
if err != nil {
return nil, err
}
if metrics.CSSContentSize == nil {
return nil, errors.New("failed to get css content size")
}
oldView := proto.EmulationSetDeviceMetricsOverride{}
set := p.LoadState(&oldView)
view := oldView
view.Width = int(metrics.CSSContentSize.Width)
view.Height = int(metrics.CSSContentSize.Height)
err = p.SetViewport(&view)
if err != nil {
return nil, err
}
defer func() { // try to recover the viewport
if !set {
_ = proto.EmulationClearDeviceMetricsOverride{}.Call(p)
return
}
_ = p.SetViewport(&oldView)
}()
}
shot, err := req.Call(p)
if err != nil {
return nil, err
}
return shot.Data, nil
}
// ScrollScreenshotOptions is the options for the ScrollScreenshot.
type ScrollScreenshotOptions struct {
// Format (optional) Image compression format (defaults to png).
Format proto.PageCaptureScreenshotFormat `json:"format,omitempty"`
// Quality (optional) Compression quality from range [0..100] (jpeg only).
Quality *int `json:"quality,omitempty"`
// FixedTop (optional) The number of pixels to skip from the top.
// It is suitable for optimizing the screenshot effect when there is a fixed
// positioning element at the top of the page.
FixedTop float64
// FixedBottom (optional) The number of pixels to skip from the bottom.
FixedBottom float64
// WaitPerScroll until no animation (default is 300ms)
WaitPerScroll time.Duration
}
// ScrollScreenshot Scroll screenshot does not adjust the size of the viewport,
// but achieves it by scrolling and capturing screenshots in a loop, and then stitching them together.
// Note that this method also has a flaw: when there are elements with fixed
// positioning on the page (usually header navigation components),
// these elements will appear repeatedly, you can set the FixedTop parameter to optimize it.
//
// Only support png and jpeg format yet, webP is not supported because no suitable processing
// library was found in golang.
func (p *Page) ScrollScreenshot(opt *ScrollScreenshotOptions) ([]byte, error) {
if opt == nil {
opt = &ScrollScreenshotOptions{}
}
if opt.WaitPerScroll == 0 {
opt.WaitPerScroll = time.Millisecond * 300
}
metrics, err := proto.PageGetLayoutMetrics{}.Call(p)
if err != nil {
return nil, err
}
if metrics.CSSContentSize == nil || metrics.CSSVisualViewport == nil {
return nil, errors.New("failed to get css content size")
}
viewpointHeight := metrics.CSSVisualViewport.ClientHeight
contentHeight := metrics.CSSContentSize.Height
var scrollTop float64
var images []utils.ImgWithBox
for {
clip := &proto.PageViewport{
X: 0,
Y: scrollTop,
Width: metrics.CSSVisualViewport.ClientWidth,
Scale: 1,
}
scrollY := viewpointHeight - (opt.FixedTop + opt.FixedBottom)
if scrollTop+viewpointHeight > contentHeight {
clip.Height = contentHeight - scrollTop
} else {
clip.Height = scrollY
if scrollTop != 0 {
clip.Y += opt.FixedTop
}
}
req := &proto.PageCaptureScreenshot{
Format: opt.Format,
Quality: opt.Quality,
Clip: clip,
FromSurface: false,
CaptureBeyondViewport: false,
OptimizeForSpeed: false,
}
shot, err := req.Call(p)
if err != nil {
return nil, err
}
images = append(images, utils.ImgWithBox{Img: shot.Data})
scrollTop += scrollY
if scrollTop >= contentHeight {
break
}
err = p.Mouse.Scroll(0, scrollY, 1)
if err != nil {
return nil, fmt.Errorf("scroll error: %w", err)
}
err = p.WaitDOMStable(opt.WaitPerScroll, 0)
if err != nil {
return nil, fmt.Errorf("WaitDOMStable error: %w", err)
}
}
var imgOption *utils.ImgOption
if opt.Quality != nil {
imgOption = &utils.ImgOption{
Quality: *opt.Quality,
}
}
bs, err := utils.SplicePngVertical(images, opt.Format, imgOption)
if err != nil {
return nil, err
}
return bs, 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
res, err := req.Call(p)
if err != nil {
return nil, err
}
return NewStreamReader(p, res.Stream), nil
}
// GetResource content by the url. Such as image, css, html, etc.
// Use the [proto.PageGetResourceTree] to list all the resources.
func (p *Page) GetResource(url string) ([]byte, error) {
res, err := proto.PageGetResourceContent{
FrameID: p.FrameID,
URL: url,
}.Call(p)
if err != nil {
return nil, err
}
data := res.Content
var bin []byte
if res.Base64Encoded {
bin, err = base64.StdEncoding.DecodeString(data)
utils.E(err)
} else {
bin = []byte(data)
}
return bin, nil
}
// WaitOpen waits for the next new page opened by the current one.
func (p *Page) WaitOpen() func() (*Page, error) {
var targetID proto.TargetTargetID
b := p.browser.Context(p.ctx)
wait := b.EachEvent(func(e *proto.TargetTargetCreated) bool {
targetID = e.TargetInfo.TargetID
return e.TargetInfo.OpenerID == p.TargetID
})
return func() (*Page, error) {
defer p.tryTrace(TraceTypeWait, "wait open")()
wait()
return b.PageFromTarget(targetID)
}
}
// EachEvent of the specified event types, if any callback returns true the wait function will resolve,
// The type of each callback is (? means optional):
//
// func(proto.Event, proto.TargetSessionID?) bool?
//
// You can listen to multiple event types at the same time like:
//
// browser.EachEvent(func(a *proto.A) {}, func(b *proto.B) {})
//
// Such as subscribe the events to know when the navigation is complete or when the page is rendered.
// Here's an example to dismiss all dialogs/alerts on the page:
//
// go page.EachEvent(func(e *proto.PageJavascriptDialogOpening) {
// _ = proto.PageHandleJavaScriptDialog{ Accept: false, PromptText: ""}.Call(page)
// })()
func (p *Page) EachEvent(callbacks ...interface{}) (wait func()) {
return p.browser.Context(p.ctx).eachEvent(p.SessionID, callbacks...)
}
// WaitEvent waits for the next event for one time. It will also load the data into the event object.
func (p *Page) WaitEvent(e proto.Event) (wait func()) {
defer p.tryTrace(TraceTypeWait, "event", e.ProtoEvent())()
return p.browser.Context(p.ctx).waitEvent(p.SessionID, e)
}
// WaitNavigation wait for a page lifecycle event when navigating.
// Usually you will wait for [proto.PageLifecycleEventNameNetworkAlmostIdle].
func (p *Page) WaitNavigation(name proto.PageLifecycleEventName) func() {
_ = proto.PageSetLifecycleEventsEnabled{Enabled: true}.Call(p)
wait := p.EachEvent(func(e *proto.PageLifecycleEvent) bool {
return e.Name == name
})
return func() {
defer p.tryTrace(TraceTypeWait, "navigation", name)()
wait()
_ = proto.PageSetLifecycleEventsEnabled{Enabled: false}.Call(p)
}
}
// WaitRequestIdle returns a wait function that waits until no request for d duration.
// Be careful, d is not the max wait timeout, it's the least idle time.
// If you want to set a timeout you can use the [Page.Timeout] function.
// Use the includes and excludes regexp list to filter the requests by their url.
func (p *Page) WaitRequestIdle(
d time.Duration,
includes, excludes []string,
excludeTypes []proto.NetworkResourceType,
) func() {
defer p.tryTrace(TraceTypeWait, "request-idle")()
if excludeTypes == nil {
excludeTypes = []proto.NetworkResourceType{
proto.NetworkResourceTypeWebSocket,
proto.NetworkResourceTypeEventSource,
proto.NetworkResourceTypeMedia,
proto.NetworkResourceTypeImage,
proto.NetworkResourceTypeFont,
}
}
if len(includes) == 0 {
includes = []string{""}
}
p, cancel := p.WithCancel()
match := genRegMatcher(includes, excludes)
waitList := map[proto.NetworkRequestID]string{}
idleCounter := utils.NewIdleCounter(d)
update := p.tryTraceReq(includes, excludes)
update(nil)
checkDone := func(id proto.NetworkRequestID) {
if _, has := waitList[id]; has {
delete(waitList, id)
update(waitList)
idleCounter.Done()
}
}
wait := p.EachEvent(func(sent *proto.NetworkRequestWillBeSent) {
for _, t := range excludeTypes {
if sent.Type == t {
return
}
}
if match(sent.Request.URL) {
// Redirect will send multiple NetworkRequestWillBeSent events with the same RequestID,
// we should filter them out.
if _, has := waitList[sent.RequestID]; !has {
waitList[sent.RequestID] = sent.Request.URL
update(waitList)
idleCounter.Add()
}
}
}, func(e *proto.NetworkLoadingFinished) {
checkDone(e.RequestID)
}, func(e *proto.NetworkLoadingFailed) {
checkDone(e.RequestID)
})
return func() {
go func() {
idleCounter.Wait(p.ctx)
cancel()
}()
wait()
}
}
// WaitDOMStable waits until the change of the DOM tree is less or equal than diff percent for d duration.
// 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) WaitDOMStable(d time.Duration, diff float64) error {
defer p.tryTrace(TraceTypeWait, "dom-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)
lcs := xs.YadLCS(p.ctx, ys)
df := 1 - float64(len(lcs))/float64(len(ys))
if df <= diff {
break
}
domSnapshot = currentDomSnapshot
}
return nil
}
// WaitStable waits until the page is stable for d duration.
func (p *Page) WaitStable(d time.Duration) error {
defer p.tryTrace(TraceTypeWait, "stable")()
var err error
setErr := sync.Once{}
utils.All(func() {
e := p.WaitLoad()
setErr.Do(func() { err = e })
}, func() {
p.WaitRequestIdle(d, nil, nil, nil)()
}, func() {
e := p.WaitDOMStable(d, 0)
setErr.Do(func() { err = e })
})()
return err
}
// 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())
return err
}
// WaitRepaint waits until the next repaint.
// 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))`)
return err
}
// WaitLoad waits for the `window.onload` event, it returns immediately if the event is already fired.
func (p *Page) WaitLoad() error {
defer p.tryTrace(TraceTypeWait, "load")()
_, err := p.Evaluate(evalHelper(js.WaitLoad).ByPromise())
return err
}
// AddScriptTag to page. If url is empty, content will be used.
func (p *Page) AddScriptTag(url, content string) error {
hash := md5.Sum([]byte(url + content))
id := hex.EncodeToString(hash[:])
_, err := p.Evaluate(evalHelper(js.AddScriptTag, id, url, content).ByPromise())
return err
}
// AddStyleTag to page. If url is empty, content will be used.
func (p *Page) AddStyleTag(url, content string) error {
hash := md5.Sum([]byte(url + content))
id := hex.EncodeToString(hash[:])
_, err := p.Evaluate(evalHelper(js.AddStyleTag, id, url, content).ByPromise())
return err
}
// EvalOnNewDocument Evaluates given script in every frame upon creation (before loading frame's scripts).
func (p *Page) EvalOnNewDocument(js string) (remove func() error, err error) {
res, err := proto.PageAddScriptToEvaluateOnNewDocument{Source: js}.Call(p)
if err != nil {
return
}
remove = func() error {
return proto.PageRemoveScriptToEvaluateOnNewDocument{
Identifier: res.Identifier,
}.Call(p)
}
return
}
// Wait until the js returns true.
func (p *Page) Wait(opts *EvalOptions) error {
return utils.Retry(p.ctx, p.sleeper(), func() (bool, error) {
res, err := p.Evaluate(opts)
if err != nil {
return true, err
}
return res.Value.Bool(), nil
})
}
// WaitElementsMoreThan waits until there are more than num elements that match the selector.
func (p *Page) WaitElementsMoreThan(selector string, num int) error {
return p.Wait(Eval(`(s, n) => document.querySelectorAll(s).length > n`, selector, num))
}
// ObjectToJSON by object id.
func (p *Page) ObjectToJSON(obj *proto.RuntimeRemoteObject) (gson.JSON, error) {
if obj.ObjectID == "" {
return obj.Value, nil
}
res, err := proto.RuntimeCallFunctionOn{
ObjectID: obj.ObjectID,
FunctionDeclaration: `function() { return this }`,
ReturnByValue: true,
}.Call(p)
if err != nil {
return gson.New(nil), err
}
return res.Result.Value, nil
}
// ElementFromObject creates an Element from the remote object id.
func (p *Page) ElementFromObject(obj *proto.RuntimeRemoteObject) (*Element, error) {
// If the element is in an iframe, we need the jsCtxID to inject helper.js to the correct context.
id, err := p.jsCtxIDByObjectID(obj.ObjectID)
if err != nil {
return nil, err
}
pid, err := p.getJSCtxID()
if err != nil {
return nil, err
}
if id != pid {
clone := *p
clone.jsCtxID = &id
p = &clone
}
return &Element{
e: p.e,
ctx: p.ctx,
sleeper: p.sleeper,
page: p,
Object: obj,
}, nil
}
// ElementFromNode creates an Element from the node, [proto.DOMNodeID] or [proto.DOMBackendNodeID] must be specified.
func (p *Page) ElementFromNode(node *proto.DOMNode) (*Element, error) {
res, err := proto.DOMResolveNode{
NodeID: node.NodeID,
BackendNodeID: node.BackendNodeID,
}.Call(p)
if err != nil {
return nil, err
}
el, err := p.ElementFromObject(res.Object)
if err != nil {
return nil, err
}
// make sure always return an element node
desc, err := el.Describe(0, false)
if err != nil {
return nil, err
}
if desc.NodeName == "#text" {
el, err = el.Parent()
if err != nil {
return nil, err
}
}
return el, nil
}
// ElementFromPoint creates an Element from the absolute point on the page.
// The point should include the window scroll offset.
func (p *Page) ElementFromPoint(x, y int) (*Element, error) {
node, err := proto.DOMGetNodeForLocation{X: x, Y: y}.Call(p)
if err != nil {
return nil, err
}
return p.ElementFromNode(&proto.DOMNode{
BackendNodeID: node.BackendNodeID,
})
}
// Release the remote object. Usually, you don't need to call it.
// When a page is closed or reloaded, all remote objects will be released automatically.
// It's useful if the page never closes or reloads.
func (p *Page) Release(obj *proto.RuntimeRemoteObject) error {
err := proto.RuntimeReleaseObject{ObjectID: obj.ObjectID}.Call(p)
return err
}
// Call implements the [proto.Client].
func (p *Page) Call(ctx context.Context, sessionID, methodName string, params interface{}) (res []byte, err error) {
return p.browser.Call(ctx, sessionID, methodName, params)
}
// Event of the page.
func (p *Page) Event() <-chan *Message {
dst := make(chan *Message)
s := p.event.Subscribe(p.ctx)
go func() {
defer close(dst)
for {
select {
case <-p.ctx.Done():
return
case msg, ok := <-s:
if !ok {
return
}
select {
case <-p.ctx.Done():
return
case dst <- msg.(*Message): //nolint: forcetypeassert
}
}
}
}()
return dst
}
func (p *Page) initEvents() {
p.event = goob.New(p.ctx)
event := p.browser.Context(p.ctx).Event()
go func() {
for msg := range event {
detached := proto.TargetDetachedFromTarget{}
destroyed := proto.TargetTargetDestroyed{}
if (msg.Load(&detached) && detached.SessionID == p.SessionID) ||
(msg.Load(destroyed) && destroyed.TargetID == p.TargetID) {
p.sessionCancel()
return
}
if msg.SessionID != p.SessionID {
continue
}
p.event.Publish(msg)
}
}()
}