mirror of
https://github.com/MontFerret/ferret.git
synced 2025-05-15 22:26:40 +02:00
#23 Added NAVIGATE
This commit is contained in:
parent
3854bb39ce
commit
a6b51a1f40
13
docs/examples/navigate.fql
Normal file
13
docs/examples/navigate.fql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
LET doc = DOCUMENT("https://github.com/", true)
|
||||||
|
|
||||||
|
LET main = ELEMENT(doc, '.application-main')
|
||||||
|
LOG('innerText:start')
|
||||||
|
LET mainTxt = main.innerText
|
||||||
|
LOG('innerText:end')
|
||||||
|
|
||||||
|
NAVIGATE(doc, "https://github.com/features")
|
||||||
|
|
||||||
|
LET features = ELEMENT(doc, '.application-main')
|
||||||
|
LET featuresTxt = features.innerText
|
||||||
|
|
||||||
|
RETURN mainTxt == featuresTxt
|
@ -1,6 +1,7 @@
|
|||||||
package values
|
package values
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -210,6 +211,18 @@ func Parse(input interface{}) core.Value {
|
|||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Unmarshal(value json.RawMessage) (core.Value, error) {
|
||||||
|
var o interface{}
|
||||||
|
|
||||||
|
err := json.Unmarshal(value, &o)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parse(o), nil
|
||||||
|
}
|
||||||
|
|
||||||
func ToBoolean(input core.Value) core.Value {
|
func ToBoolean(input core.Value) core.Value {
|
||||||
switch input.Type() {
|
switch input.Type() {
|
||||||
case core.BooleanType:
|
case core.BooleanType:
|
||||||
|
@ -8,7 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
*
|
* Dispatches click event on a given element
|
||||||
|
* @param source (Document | Element) - Event source.
|
||||||
|
* @param selector (String, optional) - Optional selector. Only used when a document instance is passed.
|
||||||
*/
|
*/
|
||||||
func Click(_ context.Context, args ...core.Value) (core.Value, error) {
|
func Click(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||||
err := core.ValidateArgs(args, 1, 2)
|
err := core.ValidateArgs(args, 1, 2)
|
||||||
@ -54,3 +56,38 @@ func Click(_ context.Context, args ...core.Value) (core.Value, error) {
|
|||||||
return doc.ClickBySelector(values.NewString(selector))
|
return doc.ClickBySelector(values.NewString(selector))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Navigates a document to a new resource.
|
||||||
|
* The operation blocks the execution until the page gets loaded.
|
||||||
|
* Which means there is no need in WAIT_NAVIGATION function.
|
||||||
|
* @param doc (Document) - Target document.
|
||||||
|
* @param url (String) - Target url to navigate.
|
||||||
|
*/
|
||||||
|
func Navigate(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||||
|
err := core.ValidateArgs(args, 2, 2)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.ValidateType(args[0], core.HtmlDocumentType)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = core.ValidateType(args[1], core.StringType)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
doc, ok := args[0].(*browser.HtmlDocument)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.None, doc.Navigate(args[1].(values.String))
|
||||||
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/mafredri/cdp/protocol/emulation"
|
"github.com/mafredri/cdp/protocol/emulation"
|
||||||
"github.com/mafredri/cdp/protocol/page"
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
"github.com/mafredri/cdp/rpcc"
|
"github.com/mafredri/cdp/rpcc"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@ -43,7 +44,7 @@ func LoadHtmlDocument(
|
|||||||
|
|
||||||
client := cdp.NewClient(conn)
|
client := cdp.NewClient(conn)
|
||||||
|
|
||||||
err := RunBatch(
|
err := runBatch(
|
||||||
func() error {
|
func() error {
|
||||||
return client.Page.Enable(ctx)
|
return client.Page.Enable(ctx)
|
||||||
},
|
},
|
||||||
@ -96,25 +97,9 @@ func LoadHtmlDocument(
|
|||||||
return NewHtmlDocument(conn, client, root, broker), nil
|
return NewHtmlDocument(conn, client, root, broker), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForLoadEvent(ctx context.Context, client *cdp.Client) error {
|
|
||||||
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = loadEventFired.Recv()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return loadEventFired.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRootElement(client *cdp.Client) (dom.Node, error) {
|
func getRootElement(client *cdp.Client) (dom.Node, error) {
|
||||||
args := dom.NewGetDocumentArgs()
|
args := dom.NewGetDocumentArgs()
|
||||||
args.Depth = PointerInt(1) // lets load the entire document
|
args.Depth = pointerInt(1) // lets load the entire document
|
||||||
|
|
||||||
d, err := client.DOM.GetDocument(context.Background(), args)
|
d, err := client.DOM.GetDocument(context.Background(), args)
|
||||||
|
|
||||||
@ -419,3 +404,18 @@ func (doc *HtmlDocument) WaitForNavigation(timeout values.Int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (doc *HtmlDocument) Navigate(url values.String) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
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 waitForLoadEvent(ctx, doc.client)
|
||||||
|
}
|
||||||
|
@ -2,11 +2,11 @@ package eval
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||||
"github.com/mafredri/cdp"
|
"github.com/mafredri/cdp"
|
||||||
|
"github.com/mafredri/cdp/protocol/dom"
|
||||||
"github.com/mafredri/cdp/protocol/runtime"
|
"github.com/mafredri/cdp/protocol/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -36,15 +36,82 @@ func Eval(client *cdp.Client, exp string, ret bool, async bool) (core.Value, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
if out.Result.Type != "undefined" {
|
if out.Result.Type != "undefined" {
|
||||||
var o interface{}
|
return values.Unmarshal(out.Result.Value)
|
||||||
|
|
||||||
err := json.Unmarshal(out.Result.Value, &o)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return values.None, core.Error(core.ErrUnexpected, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return values.Parse(o), nil
|
return Unmarshal(&out.Result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Property(
|
||||||
|
ctx context.Context,
|
||||||
|
client *cdp.Client,
|
||||||
|
id dom.NodeID,
|
||||||
|
propName string,
|
||||||
|
) (core.Value, error) {
|
||||||
|
// get a ref to remote object representing the node
|
||||||
|
obj, err := client.DOM.ResolveNode(
|
||||||
|
ctx,
|
||||||
|
dom.NewResolveNodeArgs().
|
||||||
|
SetNodeID(id),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Object.ObjectID == nil {
|
||||||
|
return values.None, core.Error(core.ErrNotFound, fmt.Sprintf("element %d", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
defer client.Runtime.ReleaseObject(ctx, runtime.NewReleaseObjectArgs(*obj.Object.ObjectID))
|
||||||
|
|
||||||
|
res, err := client.Runtime.GetProperties(
|
||||||
|
ctx,
|
||||||
|
runtime.NewGetPropertiesArgs(*obj.Object.ObjectID),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.ExceptionDetails != nil {
|
||||||
|
return values.None, res.ExceptionDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
// all props
|
||||||
|
if propName == "" {
|
||||||
|
var arr *values.Array
|
||||||
|
arr = values.NewArray(len(res.Result))
|
||||||
|
|
||||||
|
for _, prop := range res.Result {
|
||||||
|
val, err := Unmarshal(prop.Value)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.Push(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prop := range res.Result {
|
||||||
|
if prop.Name == propName {
|
||||||
|
return Unmarshal(prop.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.None, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unmarshal(obj *runtime.RemoteObject) (core.Value, error) {
|
||||||
|
if obj == nil {
|
||||||
|
return values.None, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Type != "undefined" {
|
||||||
|
return values.Unmarshal(obj.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return values.None, nil
|
return values.None, nil
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
package browser
|
package browser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/mafredri/cdp"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PointerInt(input int) *int {
|
func pointerInt(input int) *int {
|
||||||
return &input
|
return &input
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatchFunc = func() error
|
type batchFunc = func() error
|
||||||
|
|
||||||
func RunBatch(funcs ...BatchFunc) error {
|
func runBatch(funcs ...batchFunc) error {
|
||||||
eg := errgroup.Group{}
|
eg := errgroup.Group{}
|
||||||
|
|
||||||
for _, f := range funcs {
|
for _, f := range funcs {
|
||||||
@ -19,3 +21,23 @@ func RunBatch(funcs ...BatchFunc) error {
|
|||||||
|
|
||||||
return eg.Wait()
|
return eg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contextWithTimeout() (context.Context, context.CancelFunc) {
|
||||||
|
return context.WithTimeout(context.Background(), DefaultTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForLoadEvent(ctx context.Context, client *cdp.Client) error {
|
||||||
|
loadEventFired, err := client.Page.LoadEventFired(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = loadEventFired.Recv()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadEventFired.Close()
|
||||||
|
}
|
||||||
|
@ -11,5 +11,6 @@ func NewLib() map[string]core.Function {
|
|||||||
"WAIT_ELEMENT": WaitElement,
|
"WAIT_ELEMENT": WaitElement,
|
||||||
"WAIT_NAVIGATION": WaitNavigation,
|
"WAIT_NAVIGATION": WaitNavigation,
|
||||||
"CLICK": Click,
|
"CLICK": Click,
|
||||||
|
"NAVIGATE": Navigate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user