mirror of
https://github.com/MontFerret/ferret.git
synced 2024-12-16 11:37:36 +02:00
1127 lines
23 KiB
Go
1127 lines
23 KiB
Go
package cdp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/MontFerret/ferret/pkg/drivers"
|
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
|
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
|
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
|
|
"github.com/MontFerret/ferret/pkg/drivers/common"
|
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
|
"github.com/MontFerret/ferret/pkg/runtime/logging"
|
|
"github.com/MontFerret/ferret/pkg/runtime/values"
|
|
"github.com/MontFerret/ferret/pkg/runtime/values/types"
|
|
"github.com/mafredri/cdp"
|
|
"github.com/mafredri/cdp/protocol/dom"
|
|
"github.com/mafredri/cdp/protocol/input"
|
|
"github.com/mafredri/cdp/protocol/network"
|
|
"github.com/mafredri/cdp/protocol/page"
|
|
"github.com/mafredri/cdp/rpcc"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
const BlankPageURL = "about:blank"
|
|
|
|
type HTMLDocument struct {
|
|
sync.Mutex
|
|
logger *zerolog.Logger
|
|
conn *rpcc.Conn
|
|
client *cdp.Client
|
|
events *events.EventBroker
|
|
url values.String
|
|
element *HTMLElement
|
|
}
|
|
|
|
func handleLoadError(logger *zerolog.Logger, client *cdp.Client) {
|
|
err := client.Page.Close(context.Background())
|
|
|
|
if err != nil {
|
|
logger.Warn().Timestamp().Err(err).Msg("unabled to close document on load error")
|
|
}
|
|
}
|
|
|
|
func LoadHTMLDocument(
|
|
ctx context.Context,
|
|
conn *rpcc.Conn,
|
|
client *cdp.Client,
|
|
params drivers.LoadDocumentParams,
|
|
) (drivers.HTMLDocument, error) {
|
|
logger := logging.FromContext(ctx)
|
|
|
|
if conn == nil {
|
|
return nil, core.Error(core.ErrMissedArgument, "connection")
|
|
}
|
|
|
|
if params.URL == "" {
|
|
return nil, core.Error(core.ErrMissedArgument, "url")
|
|
}
|
|
|
|
if params.Cookies != nil {
|
|
cookies := make([]network.CookieParam, 0, len(params.Cookies))
|
|
|
|
for _, c := range params.Cookies {
|
|
cookies = append(cookies, fromDriverCookie(params.URL, c))
|
|
|
|
logger.
|
|
Debug().
|
|
Timestamp().
|
|
Str("cookie", c.Name).
|
|
Msg("set cookie")
|
|
}
|
|
|
|
err := client.Network.SetCookies(
|
|
ctx,
|
|
network.NewSetCookiesArgs(cookies),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if params.Header != nil {
|
|
j, err := json.Marshal(params.Header)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for k := range params.Header {
|
|
logger.
|
|
Debug().
|
|
Timestamp().
|
|
Str("header", k).
|
|
Msg("set header")
|
|
}
|
|
|
|
err = client.Network.SetExtraHTTPHeaders(
|
|
ctx,
|
|
network.NewSetExtraHTTPHeadersArgs(network.Headers(j)),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var err error
|
|
|
|
if params.URL != BlankPageURL {
|
|
err = waitForLoadEvent(ctx, client)
|
|
|
|
if err != nil {
|
|
handleLoadError(logger, client)
|
|
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
node, err := getRootElement(ctx, client)
|
|
|
|
if err != nil {
|
|
handleLoadError(logger, client)
|
|
return nil, errors.Wrap(err, "failed to get root element")
|
|
}
|
|
|
|
broker, err := createEventBroker(client)
|
|
|
|
if err != nil {
|
|
handleLoadError(logger, client)
|
|
return nil, errors.Wrap(err, "failed to create event events")
|
|
}
|
|
|
|
rootElement, err := LoadElement(
|
|
ctx,
|
|
logger,
|
|
client,
|
|
broker,
|
|
node.Root.NodeID,
|
|
node.Root.BackendNodeID,
|
|
)
|
|
|
|
if err != nil {
|
|
broker.Stop()
|
|
broker.Close()
|
|
handleLoadError(logger, client)
|
|
|
|
return nil, errors.Wrap(err, "failed to load root element")
|
|
}
|
|
|
|
return NewHTMLDocument(
|
|
logger,
|
|
conn,
|
|
client,
|
|
broker,
|
|
values.NewString(params.URL),
|
|
rootElement,
|
|
), nil
|
|
}
|
|
|
|
func NewHTMLDocument(
|
|
logger *zerolog.Logger,
|
|
conn *rpcc.Conn,
|
|
client *cdp.Client,
|
|
broker *events.EventBroker,
|
|
url values.String,
|
|
rootElement *HTMLElement,
|
|
) *HTMLDocument {
|
|
doc := new(HTMLDocument)
|
|
doc.logger = logger
|
|
doc.conn = conn
|
|
doc.client = client
|
|
doc.events = broker
|
|
doc.url = url
|
|
doc.element = rootElement
|
|
|
|
broker.AddEventListener(events.EventLoad, doc.handlePageLoad)
|
|
broker.AddEventListener(events.EventError, doc.handleError)
|
|
|
|
return doc
|
|
}
|
|
|
|
func (doc *HTMLDocument) MarshalJSON() ([]byte, error) {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.MarshalJSON()
|
|
}
|
|
|
|
func (doc *HTMLDocument) Type() core.Type {
|
|
return drivers.HTMLDocumentType
|
|
}
|
|
|
|
func (doc *HTMLDocument) String() string {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.url.String()
|
|
}
|
|
|
|
func (doc *HTMLDocument) Unwrap() interface{} {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element
|
|
}
|
|
|
|
func (doc *HTMLDocument) Hash() uint64 {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
h := fnv.New64a()
|
|
|
|
h.Write([]byte(doc.Type().String()))
|
|
h.Write([]byte(":"))
|
|
h.Write([]byte(doc.url))
|
|
|
|
return h.Sum64()
|
|
}
|
|
|
|
func (doc *HTMLDocument) Copy() core.Value {
|
|
return values.None
|
|
}
|
|
|
|
func (doc *HTMLDocument) Compare(other core.Value) int64 {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
switch other.Type() {
|
|
case drivers.HTMLDocumentType:
|
|
other := other.(drivers.HTMLDocument)
|
|
|
|
return doc.url.Compare(other.GetURL())
|
|
default:
|
|
return drivers.Compare(doc.Type(), other.Type())
|
|
}
|
|
}
|
|
|
|
func (doc *HTMLDocument) Iterate(ctx context.Context) (core.Iterator, error) {
|
|
return doc.element.Iterate(ctx)
|
|
}
|
|
|
|
func (doc *HTMLDocument) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
|
|
return common.GetInDocument(ctx, doc, path)
|
|
}
|
|
|
|
func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
|
|
return common.SetInDocument(ctx, doc, path, value)
|
|
}
|
|
|
|
func (doc *HTMLDocument) Close() error {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
err := doc.events.Stop()
|
|
|
|
if err != nil {
|
|
doc.logger.Warn().
|
|
Timestamp().
|
|
Str("url", doc.url.String()).
|
|
Err(err).
|
|
Msg("failed to stop event events")
|
|
}
|
|
|
|
err = doc.events.Close()
|
|
|
|
if err != nil {
|
|
doc.logger.Warn().
|
|
Timestamp().
|
|
Str("url", doc.url.String()).
|
|
Err(err).
|
|
Msg("failed to close event events")
|
|
}
|
|
|
|
err = doc.element.Close()
|
|
|
|
if err != nil {
|
|
doc.logger.Warn().
|
|
Timestamp().
|
|
Str("url", doc.url.String()).
|
|
Err(err).
|
|
Msg("failed to close root element")
|
|
}
|
|
|
|
err = doc.client.Page.Close(context.Background())
|
|
|
|
if err != nil {
|
|
doc.logger.Warn().
|
|
Timestamp().
|
|
Str("url", doc.url.String()).
|
|
Err(err).
|
|
Msg("failed to close browser page")
|
|
}
|
|
|
|
return doc.conn.Close()
|
|
}
|
|
|
|
func (doc *HTMLDocument) NodeType() values.Int {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.NodeType()
|
|
}
|
|
|
|
func (doc *HTMLDocument) NodeName() values.String {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.NodeName()
|
|
}
|
|
|
|
func (doc *HTMLDocument) Length() values.Int {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.Length()
|
|
}
|
|
|
|
func (doc *HTMLDocument) GetChildNodes(ctx context.Context) core.Value {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.GetChildNodes(ctx)
|
|
}
|
|
|
|
func (doc *HTMLDocument) GetChildNode(ctx context.Context, idx values.Int) core.Value {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.GetChildNode(ctx, idx)
|
|
}
|
|
|
|
func (doc *HTMLDocument) QuerySelector(ctx context.Context, selector values.String) core.Value {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.QuerySelector(ctx, selector)
|
|
}
|
|
|
|
func (doc *HTMLDocument) QuerySelectorAll(ctx context.Context, selector values.String) core.Value {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.QuerySelectorAll(ctx, selector)
|
|
}
|
|
|
|
func (doc *HTMLDocument) DocumentElement() drivers.HTMLElement {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element
|
|
}
|
|
|
|
func (doc *HTMLDocument) GetURL() core.Value {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.url
|
|
}
|
|
|
|
func (doc *HTMLDocument) GetCookies(ctx context.Context) (*values.Array, error) {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
repl, err := doc.client.Network.GetAllCookies(ctx)
|
|
|
|
if err != nil {
|
|
return values.NewArray(0), err
|
|
}
|
|
|
|
if repl.Cookies == nil {
|
|
return values.NewArray(0), nil
|
|
}
|
|
|
|
cookies := values.NewArray(len(repl.Cookies))
|
|
|
|
for _, c := range repl.Cookies {
|
|
cookies.Push(toDriverCookie(c))
|
|
}
|
|
|
|
return cookies, nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) SetCookies(ctx context.Context, cookies ...drivers.HTTPCookie) error {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
if len(cookies) == 0 {
|
|
return nil
|
|
}
|
|
|
|
params := make([]network.CookieParam, 0, len(cookies))
|
|
|
|
for _, c := range cookies {
|
|
params = append(params, fromDriverCookie(doc.url.String(), c))
|
|
}
|
|
|
|
return doc.client.Network.SetCookies(ctx, network.NewSetCookiesArgs(params))
|
|
}
|
|
|
|
func (doc *HTMLDocument) DeleteCookies(ctx context.Context, cookies ...drivers.HTTPCookie) error {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
if len(cookies) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
|
|
for _, c := range cookies {
|
|
err = doc.client.Network.DeleteCookies(ctx, fromDriverCookieDelete(doc.url.String(), c))
|
|
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) SetURL(ctx context.Context, url values.String) error {
|
|
return doc.Navigate(ctx, url)
|
|
}
|
|
|
|
func (doc *HTMLDocument) CountBySelector(ctx context.Context, selector values.String) values.Int {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.CountBySelector(ctx, selector)
|
|
}
|
|
|
|
func (doc *HTMLDocument) ExistsBySelector(ctx context.Context, selector values.String) values.Boolean {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
return doc.element.ExistsBySelector(ctx, selector)
|
|
}
|
|
|
|
func (doc *HTMLDocument) ClickBySelector(ctx context.Context, selector values.String) (values.Boolean, error) {
|
|
res, err := eval.Eval(
|
|
ctx,
|
|
doc.client,
|
|
fmt.Sprintf(`
|
|
var el = document.querySelector(%s);
|
|
if (el == null) {
|
|
return false;
|
|
}
|
|
var evt = new window.MouseEvent('click', { bubbles: true, cancelable: true });
|
|
el.dispatchEvent(evt);
|
|
return true;
|
|
`, eval.ParamString(selector.String())),
|
|
true,
|
|
false,
|
|
)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
if res.Type() == types.Boolean {
|
|
return res.(values.Boolean), nil
|
|
}
|
|
|
|
return values.False, nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) ClickBySelectorAll(ctx context.Context, selector values.String) (values.Boolean, error) {
|
|
res, err := eval.Eval(
|
|
ctx,
|
|
doc.client,
|
|
fmt.Sprintf(`
|
|
var elements = document.querySelectorAll(%s);
|
|
if (elements == null) {
|
|
return false;
|
|
}
|
|
elements.forEach((el) => {
|
|
var evt = new window.MouseEvent('click', { bubbles: true, cancelable: true });
|
|
el.dispatchEvent(evt);
|
|
});
|
|
return true;
|
|
`, eval.ParamString(selector.String())),
|
|
true,
|
|
false,
|
|
)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
if res.Type() == types.Boolean {
|
|
return res.(values.Boolean), nil
|
|
}
|
|
|
|
return values.False, nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) InputBySelector(ctx context.Context, selector values.String, value core.Value, delay values.Int) (values.Boolean, error) {
|
|
valStr := value.String()
|
|
|
|
res, err := eval.Eval(
|
|
ctx,
|
|
doc.client,
|
|
fmt.Sprintf(`
|
|
var el = document.querySelector(%s);
|
|
if (el == null) {
|
|
return false;
|
|
}
|
|
el.focus();
|
|
return true;
|
|
`, eval.ParamString(selector.String())),
|
|
true,
|
|
false,
|
|
)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
if res.Type() == types.Boolean && res.(values.Boolean) == values.False {
|
|
return values.False, nil
|
|
}
|
|
|
|
// Initial delay after focusing but before typing
|
|
time.Sleep(time.Duration(delay) * time.Millisecond)
|
|
|
|
for _, ch := range valStr {
|
|
for _, ev := range []string{"keyDown", "keyUp"} {
|
|
ke := input.NewDispatchKeyEventArgs(ev).SetText(string(ch))
|
|
|
|
if err := doc.client.Input.DispatchKeyEvent(ctx, ke); err != nil {
|
|
return values.False, err
|
|
}
|
|
}
|
|
|
|
time.Sleep(randomDuration(delay) * time.Millisecond)
|
|
}
|
|
|
|
return values.True, nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) SelectBySelector(ctx context.Context, selector values.String, value *values.Array) (*values.Array, error) {
|
|
res, err := eval.Eval(
|
|
ctx,
|
|
doc.client,
|
|
fmt.Sprintf(`
|
|
var element = document.querySelector(%s);
|
|
if (element == null) {
|
|
return [];
|
|
}
|
|
var values = %s;
|
|
if (element.nodeName.toLowerCase() !== 'select') {
|
|
throw new Error('Element is not a <select> element.');
|
|
}
|
|
var options = Array.from(element.options);
|
|
element.value = undefined;
|
|
for (var option of options) {
|
|
option.selected = values.includes(option.value);
|
|
|
|
if (option.selected && !element.multiple) {
|
|
break;
|
|
}
|
|
}
|
|
element.dispatchEvent(new Event('input', { 'bubbles': true, cancelable: true }));
|
|
element.dispatchEvent(new Event('change', { 'bubbles': true, cancelable: true }));
|
|
|
|
return options.filter(option => option.selected).map(option => option.value);
|
|
`,
|
|
eval.ParamString(selector.String()),
|
|
value.String(),
|
|
),
|
|
true,
|
|
false,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
arr, ok := res.(*values.Array)
|
|
|
|
if ok {
|
|
return arr, nil
|
|
}
|
|
|
|
return nil, core.TypeError(types.Array, res.Type())
|
|
}
|
|
|
|
func (doc *HTMLDocument) MoveMouseBySelector(ctx context.Context, selector values.String) error {
|
|
err := doc.ScrollBySelector(ctx, selector)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
selectorArgs := dom.NewQuerySelectorArgs(doc.element.id.nodeID, selector.String())
|
|
found, err := doc.client.DOM.QuerySelector(ctx, selectorArgs)
|
|
|
|
if err != nil {
|
|
doc.element.logError(err).
|
|
Str("selector", selector.String()).
|
|
Msg("failed to retrieve a node by selector")
|
|
|
|
return err
|
|
}
|
|
|
|
if found.NodeID <= 0 {
|
|
return errors.New("element not found")
|
|
}
|
|
|
|
q, err := getClickablePoint(ctx, doc.client, &HTMLElementIdentity{
|
|
nodeID: found.NodeID,
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return doc.client.Input.DispatchMouseEvent(
|
|
ctx,
|
|
input.NewDispatchMouseEventArgs("mouseMoved", q.X, q.Y),
|
|
)
|
|
}
|
|
|
|
func (doc *HTMLDocument) MoveMouseByXY(ctx context.Context, x, y values.Float) error {
|
|
return doc.client.Input.DispatchMouseEvent(
|
|
ctx,
|
|
input.NewDispatchMouseEventArgs("mouseMoved", float64(x), float64(y)),
|
|
)
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForElement(ctx context.Context, selector values.String, when drivers.WaitEvent) error {
|
|
var operator string
|
|
|
|
if when == drivers.WaitEventPresence {
|
|
operator = "!="
|
|
} else {
|
|
operator = "=="
|
|
}
|
|
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
fmt.Sprintf(
|
|
`
|
|
var el = document.querySelector(%s);
|
|
|
|
if (el %s null) {
|
|
return true;
|
|
}
|
|
|
|
// null means we need to repeat
|
|
return null;
|
|
`,
|
|
eval.ParamString(selector.String()),
|
|
operator,
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error {
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
templates.WaitBySelector(
|
|
selector,
|
|
when,
|
|
class,
|
|
fmt.Sprintf("el.className.split(' ').find(i => i === %s)", eval.ParamString(class.String())),
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error {
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
templates.WaitBySelectorAll(
|
|
selector,
|
|
when,
|
|
class,
|
|
fmt.Sprintf("el.className.split(' ').find(i => i === %s)", eval.ParamString(class.String())),
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForNavigation(ctx context.Context) error {
|
|
onEvent := make(chan struct{})
|
|
var once sync.Once
|
|
listener := func(_ context.Context, _ interface{}) {
|
|
once.Do(func() {
|
|
close(onEvent)
|
|
})
|
|
}
|
|
|
|
defer doc.events.RemoveEventListener(events.EventLoad, listener)
|
|
|
|
doc.events.AddEventListener(events.EventLoad, listener)
|
|
|
|
select {
|
|
case <-onEvent:
|
|
return nil
|
|
case <-ctx.Done():
|
|
return core.ErrTimeout
|
|
}
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForAttributeBySelector(
|
|
ctx context.Context,
|
|
selector,
|
|
name values.String,
|
|
value core.Value,
|
|
when drivers.WaitEvent,
|
|
) error {
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
templates.WaitBySelector(
|
|
selector,
|
|
when,
|
|
value,
|
|
templates.AttributeRead(name),
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForAttributeBySelectorAll(
|
|
ctx context.Context,
|
|
selector,
|
|
name values.String,
|
|
value core.Value,
|
|
when drivers.WaitEvent,
|
|
) error {
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
templates.WaitBySelectorAll(
|
|
selector,
|
|
when,
|
|
value,
|
|
templates.AttributeRead(name),
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForStyleBySelector(ctx context.Context, selector, name values.String, value core.Value, when drivers.WaitEvent) error {
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
templates.WaitBySelector(
|
|
selector,
|
|
when,
|
|
value,
|
|
templates.StyleRead(name),
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) WaitForStyleBySelectorAll(ctx context.Context, selector, name values.String, value core.Value, when drivers.WaitEvent) error {
|
|
task := events.NewEvalWaitTask(
|
|
doc.client,
|
|
templates.WaitBySelectorAll(
|
|
selector,
|
|
when,
|
|
value,
|
|
templates.StyleRead(name),
|
|
),
|
|
events.DefaultPolling,
|
|
)
|
|
|
|
_, err := task.Run(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) Navigate(ctx context.Context, url values.String) error {
|
|
if url == "" {
|
|
url = BlankPageURL
|
|
}
|
|
|
|
repl, err := doc.client.Page.Navigate(ctx, page.NewNavigateArgs(url.String()))
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if repl.ErrorText != nil {
|
|
return errors.New(*repl.ErrorText)
|
|
}
|
|
|
|
return doc.WaitForNavigation(ctx)
|
|
}
|
|
|
|
func (doc *HTMLDocument) NavigateBack(ctx context.Context, skip values.Int) (values.Boolean, error) {
|
|
history, err := doc.client.Page.GetNavigationHistory(ctx)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
// we are in the beginning
|
|
if history.CurrentIndex == 0 {
|
|
return values.False, nil
|
|
}
|
|
|
|
if skip < 1 {
|
|
skip = 1
|
|
}
|
|
|
|
to := history.CurrentIndex - int(skip)
|
|
|
|
if to < 0 {
|
|
// TODO: Return error?
|
|
return values.False, nil
|
|
}
|
|
|
|
prev := history.Entries[to]
|
|
err = doc.client.Page.NavigateToHistoryEntry(ctx, page.NewNavigateToHistoryEntryArgs(prev.ID))
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
err = doc.WaitForNavigation(ctx)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
return values.True, nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) NavigateForward(ctx context.Context, skip values.Int) (values.Boolean, error) {
|
|
history, err := doc.client.Page.GetNavigationHistory(ctx)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
length := len(history.Entries)
|
|
lastIndex := length - 1
|
|
|
|
// nowhere to go forward
|
|
if history.CurrentIndex == lastIndex {
|
|
return values.False, nil
|
|
}
|
|
|
|
if skip < 1 {
|
|
skip = 1
|
|
}
|
|
|
|
to := int(skip) + history.CurrentIndex
|
|
|
|
if to > lastIndex {
|
|
// TODO: Return error?
|
|
return values.False, nil
|
|
}
|
|
|
|
next := history.Entries[to]
|
|
err = doc.client.Page.NavigateToHistoryEntry(ctx, page.NewNavigateToHistoryEntryArgs(next.ID))
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
err = doc.WaitForNavigation(ctx)
|
|
|
|
if err != nil {
|
|
return values.False, err
|
|
}
|
|
|
|
return values.True, nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) PrintToPDF(ctx context.Context, params drivers.PDFParams) (values.Binary, error) {
|
|
args := page.NewPrintToPDFArgs()
|
|
args.
|
|
SetLandscape(bool(params.Landscape)).
|
|
SetDisplayHeaderFooter(bool(params.DisplayHeaderFooter)).
|
|
SetPrintBackground(bool(params.PrintBackground)).
|
|
SetIgnoreInvalidPageRanges(bool(params.IgnoreInvalidPageRanges)).
|
|
SetPreferCSSPageSize(bool(params.PreferCSSPageSize))
|
|
|
|
if params.Scale > 0 {
|
|
args.SetScale(float64(params.Scale))
|
|
}
|
|
|
|
if params.PaperWidth > 0 {
|
|
args.SetPaperWidth(float64(params.PaperWidth))
|
|
}
|
|
|
|
if params.PaperHeight > 0 {
|
|
args.SetPaperHeight(float64(params.PaperHeight))
|
|
}
|
|
|
|
if params.MarginTop > 0 {
|
|
args.SetMarginTop(float64(params.MarginTop))
|
|
}
|
|
|
|
if params.MarginBottom > 0 {
|
|
args.SetMarginBottom(float64(params.MarginBottom))
|
|
}
|
|
|
|
if params.MarginRight > 0 {
|
|
args.SetMarginRight(float64(params.MarginRight))
|
|
}
|
|
|
|
if params.MarginLeft > 0 {
|
|
args.SetMarginLeft(float64(params.MarginLeft))
|
|
}
|
|
|
|
if params.PageRanges != values.EmptyString {
|
|
args.SetPageRanges(string(params.PageRanges))
|
|
}
|
|
|
|
if params.HeaderTemplate != values.EmptyString {
|
|
args.SetHeaderTemplate(string(params.HeaderTemplate))
|
|
}
|
|
|
|
if params.FooterTemplate != values.EmptyString {
|
|
args.SetFooterTemplate(string(params.FooterTemplate))
|
|
}
|
|
|
|
reply, err := doc.client.Page.PrintToPDF(ctx, args)
|
|
|
|
if err != nil {
|
|
return values.NewBinary([]byte{}), err
|
|
}
|
|
|
|
return values.NewBinary(reply.Data), nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) CaptureScreenshot(ctx context.Context, params drivers.ScreenshotParams) (values.Binary, error) {
|
|
metrics, err := doc.client.Page.GetLayoutMetrics(ctx)
|
|
|
|
if err != nil {
|
|
return values.NewBinary(nil), err
|
|
}
|
|
|
|
if params.Format == drivers.ScreenshotFormatJPEG && params.Quality < 0 && params.Quality > 100 {
|
|
params.Quality = 100
|
|
}
|
|
|
|
if params.X < 0 {
|
|
params.X = 0
|
|
}
|
|
|
|
if params.Y < 0 {
|
|
params.Y = 0
|
|
}
|
|
|
|
if params.Width <= 0 {
|
|
params.Width = values.Float(metrics.LayoutViewport.ClientWidth) - params.X
|
|
}
|
|
|
|
if params.Height <= 0 {
|
|
params.Height = values.Float(metrics.LayoutViewport.ClientHeight) - params.Y
|
|
}
|
|
|
|
clip := page.Viewport{
|
|
X: float64(params.X),
|
|
Y: float64(params.Y),
|
|
Width: float64(params.Width),
|
|
Height: float64(params.Height),
|
|
Scale: 1.0,
|
|
}
|
|
|
|
format := string(params.Format)
|
|
quality := int(params.Quality)
|
|
args := page.CaptureScreenshotArgs{
|
|
Format: &format,
|
|
Quality: &quality,
|
|
Clip: &clip,
|
|
}
|
|
|
|
reply, err := doc.client.Page.CaptureScreenshot(ctx, &args)
|
|
|
|
if err != nil {
|
|
return values.NewBinary([]byte{}), err
|
|
}
|
|
|
|
return values.NewBinary(reply.Data), nil
|
|
}
|
|
|
|
func (doc *HTMLDocument) ScrollTop(ctx context.Context) error {
|
|
_, err := eval.Eval(ctx, doc.client, `
|
|
window.scrollTo({
|
|
left: 0,
|
|
top: 0,
|
|
behavior: 'instant'
|
|
});
|
|
`, false, false)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) ScrollBottom(ctx context.Context) error {
|
|
_, err := eval.Eval(ctx, doc.client, `
|
|
window.scrollTo({
|
|
left: 0,
|
|
top: window.document.body.scrollHeight,
|
|
behavior: 'instant'
|
|
});
|
|
`, false, false)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) ScrollBySelector(ctx context.Context, selector values.String) error {
|
|
_, err := eval.Eval(ctx, doc.client, fmt.Sprintf(`
|
|
var el = document.querySelector(%s);
|
|
if (el == null) {
|
|
throw new Error("element not found");
|
|
}
|
|
el.scrollIntoView({
|
|
behavior: 'instant'
|
|
});
|
|
return true;
|
|
`, eval.ParamString(selector.String()),
|
|
), false, false)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) ScrollByXY(ctx context.Context, x, y values.Float) error {
|
|
_, err := eval.Eval(ctx, doc.client, fmt.Sprintf(`
|
|
window.scrollBy({
|
|
top: %s,
|
|
left: %s,
|
|
behavior: 'instant'
|
|
});
|
|
`,
|
|
eval.ParamFloat(float64(x)),
|
|
eval.ParamFloat(float64(y)),
|
|
), false, false)
|
|
|
|
return err
|
|
}
|
|
|
|
func (doc *HTMLDocument) handlePageLoad(ctx context.Context, _ interface{}) {
|
|
doc.Lock()
|
|
defer doc.Unlock()
|
|
|
|
node, err := getRootElement(ctx, doc.client)
|
|
|
|
if err != nil {
|
|
doc.logger.Error().
|
|
Timestamp().
|
|
Err(err).
|
|
Msg("failed to get root node after page load")
|
|
|
|
return
|
|
}
|
|
|
|
updated, err := LoadElement(
|
|
ctx,
|
|
doc.logger,
|
|
doc.client,
|
|
doc.events,
|
|
node.Root.NodeID,
|
|
node.Root.BackendNodeID,
|
|
)
|
|
|
|
if err != nil {
|
|
doc.logger.Error().
|
|
Timestamp().
|
|
Err(err).
|
|
Msg("failed to load root node after page load")
|
|
|
|
return
|
|
}
|
|
|
|
// close the prev element
|
|
doc.element.Close()
|
|
|
|
// create a new root element wrapper
|
|
doc.element = updated
|
|
doc.url = ""
|
|
|
|
if node.Root.BaseURL != nil {
|
|
doc.url = values.NewString(*node.Root.BaseURL)
|
|
}
|
|
}
|
|
|
|
func (doc *HTMLDocument) handleError(_ context.Context, val interface{}) {
|
|
err, ok := val.(error)
|
|
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
doc.logger.Error().
|
|
Timestamp().
|
|
Err(err).
|
|
Msg("unexpected error")
|
|
}
|