1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-03-17 21:18:37 +02:00

Feature/#33 wait class function (#63)

* #33 Lib cleanup. Added WAIT_CLASS and WAIT_CLASS_ALL functions

* #33 Fixed attr update

* #33 HTMLElement.WaitForClass

* #33 Updated HTMLDocument.WaitForClass
This commit is contained in:
Tim Voronov 2018-10-06 22:33:39 -04:00 committed by GitHub
parent 79b8171fd8
commit e64ad4ec0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1193 additions and 670 deletions

8
examples/wait_class.fql Normal file
View File

@ -0,0 +1,8 @@
LET doc = DOCUMENT("http://getbootstrap.com/docs/4.1/components/collapse/", true)
LET el = ELEMENT(doc, "#collapseTwo")
CLICK(doc, "#headingTwo > h5 > button")
WAIT_CLASS(el, "show")
RETURN TRUE

View File

@ -0,0 +1,6 @@
LET doc = DOCUMENT("http://getbootstrap.com/docs/4.1/components/collapse/", true)
CLICK(doc, "#headingTwo > h5 > button")
WAIT_CLASS(doc, "#collapseTwo", "show")
RETURN TRUE

View File

@ -1774,29 +1774,21 @@ func TestParam(t *testing.T) {
})
}
//func TestHtml(t *testing.T) {
// Convey("Should load a document", t, func() {
// c := compiler.New()
//
// out, err := c.MustCompile(`
//LET doc = DOCUMENT("https://github.com/", true)
//LET main = ELEMENT(doc, '.application-main')
//LET mainTxt = main.innerText
//
//NAVIGATE(doc, "https://github.com/features")
//
//LET features = ELEMENT(doc, '.application-main')
//LET featuresTxt = features.innerText
//
//LOG("featuresTxt:", featuresTxt)
//
//RETURN mainTxt == featuresTxt
//
//
// `).Run(context.Background())
//
// So(err, ShouldBeNil)
//
// So(string(out), ShouldEqual, `"int"`)
// })
//}
func TestHtml(t *testing.T) {
Convey("Should load a document", t, func() {
c := compiler.New()
out, err := c.MustCompile(`
LET doc = DOCUMENT("http://getbootstrap.com/docs/4.1/components/collapse/", true)
CLICK(doc, "#headingTwo > h5 > button")
WAIT_CLASS(doc, "#collapseTwo", "bar")
RETURN TRUE
`).Run(context.Background())
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `"int"`)
})
}

View File

@ -49,3 +49,13 @@ func TypeError(actual Type, expected ...Type) error {
func Error(err error, msg string) error {
return errors.Errorf("%s: %s", err.Error(), msg)
}
func Errors(err ...error) error {
message := ""
for _, e := range err {
message += ": " + e.Error()
}
return errors.New(message)
}

View File

@ -35,5 +35,13 @@ type (
HTMLNode
URL() core.Value
InnerHTMLBySelector(selector String) String
InnerHTMLBySelectorAll(selector String) *Array
InnerTextBySelector(selector String) String
InnerTextBySelectorAll(selector String) *Array
}
)

View File

@ -10,7 +10,7 @@ import (
* Appends a new item to an array and returns a new array with a given element.
* If ``uniqueOnly`` is set to true, then will add the item only if it's unique.
* @param arr (Array) - Target array.
* @param item (Value) - Target value to add.
* @param item (Read) - Target value to add.
* @returns arr (Array) - New array.
*/
func Append(_ context.Context, args ...core.Value) (core.Value, error) {

View File

@ -9,7 +9,7 @@ import (
/*
* Returns a first element from a given array.
* @param arr (Array) - Target array.
* @returns element (Value) - First element in a given array.
* @returns element (Read) - First element in a given array.
*/
func First(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 1)

View File

@ -9,7 +9,7 @@ import (
/*
* Returns the last element of an array.
* @param array (Array) - The target array.
* @returns (Value) - Last element of an array.
* @returns (Read) - Last element of an array.
*/
func Last(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 1)

View File

@ -11,7 +11,7 @@ import (
* It is the same as anyArray[position] for positive positions, but does not support negative positions.
* @param array (Array) - An array with elements of arbitrary type.
* @param index (Int) - Position of desired element in array, positions start at 0.
* @returns (Value) - The array element at the given position.
* @returns (Read) - The array element at the given position.
* If position is negative or beyond the upper bound of the array, then NONE will be returned.
*/
func Nth(_ context.Context, args ...core.Value) (core.Value, error) {

View File

@ -9,8 +9,8 @@ import (
/*
* Returns a value indicating whether an element is contained in array. Optionally returns its position.
* @param array (Array) - The source array.
* @param value (Value) - The target value.
* @param returnIndex (Boolean, optional) - Value which indicates whether to return item's position.
* @param value (Read) - The target value.
* @param returnIndex (Boolean, optional) - Read which indicates whether to return item's position.
*/
func Position(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)

View File

@ -9,8 +9,8 @@ import (
/*
* Create a new array with appended value.
* @param array (Array) - Source array.
* @param value (Value) - Target value.
* @param unique (Boolean, optional) - Value indicating whether to do uniqueness check.
* @param value (Read) - Target value.
* @param unique (Boolean, optional) - Read indicating whether to do uniqueness check.
* @returns (Array) - A new array with appended value.
*/
func Push(_ context.Context, args ...core.Value) (core.Value, error) {

View File

@ -10,7 +10,7 @@ import (
* Returns a new array with removed all occurrences of value in a given array.
* Optionally with a limit to the number of removals.
* @param array (Array) - Source array.
* @param value (Value) - Target value.
* @param value (Read) - Target value.
* @param limit (Int, optional) - A limit to the number of removals.
* @returns (Array) - A new array with removed all occurrences of value in a given array.
*/

View File

@ -10,7 +10,7 @@ import (
* Returns a new sliced array.
* @param array (Array) - Source array.
* @param start (Int) - Start position of extraction.
* @param length (Int, optional) - Value indicating how many elements to extract.
* @param length (Int, optional) - Read indicating how many elements to extract.
* @returns (Array) - Sliced array.
*/
func Slice(_ context.Context, args ...core.Value) (core.Value, error) {

View File

@ -9,7 +9,7 @@ import (
/*
* Prepends value to a given array.
* @param array (Array) - Target array.
* @param value (Value) - Target value to prepend.
* @param value (Read) - Target value to prepend.
* @param unique (Boolean, optional) - Optional value indicating whether a value must be unique to be prepended.
* Default is false.
* @returns (Array) - New array with prepended value.

View File

@ -1,188 +0,0 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* 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) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.False, err
}
// CLICK(el)
if len(args) == 1 {
arg1 := args[0]
err := core.ValidateType(arg1, core.HTMLElementType)
if err != nil {
return values.False, err
}
el, ok := arg1.(*dynamic.HTMLElement)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic element")
}
return el.Click()
}
// CLICK(doc, selector)
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.ClickBySelector(values.NewString(selector))
}
/*
* Dispatches click event on all matched element
* @param source (Document) - Document.
* @param selector (String) - Selector.
* @returns (Boolean) - Returns true if matched at least one element.
*/
func ClickAll(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.False, err
}
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.ClickBySelectorAll(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].(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return values.None, doc.Navigate(args[1].(values.String))
}
/*
* Sends a value to an underlying input element.
* @param source (Document | Element) - Event target.
* @param valueOrSelector (String) - Selector or a value.
* @param value (String) - Target value.
* @returns (Boolean) - Returns true if an element was found.
*/
func Input(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
return values.None, err
}
// TYPE(el, "foobar")
if len(args) == 2 {
arg1 := args[0]
err := core.ValidateType(arg1, core.HTMLElementType)
if err != nil {
return values.False, err
}
el, ok := arg1.(*dynamic.HTMLElement)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic element")
}
err = el.Input(args[1])
if err != nil {
return values.False, err
}
return values.True, nil
}
arg1 := args[0]
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.False, err
}
arg2 := args[1]
err = core.ValidateType(arg2, core.StringType)
if err != nil {
return values.False, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.InputBySelector(arg2.(values.String), args[2])
}

58
pkg/stdlib/html/click.go Normal file
View File

@ -0,0 +1,58 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* 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) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.False, err
}
// CLICK(el)
if len(args) == 1 {
arg1 := args[0]
err := core.ValidateType(arg1, core.HTMLElementType)
if err != nil {
return values.False, err
}
el, ok := arg1.(*dynamic.HTMLElement)
if !ok {
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
return el.Click()
}
// CLICK(doc, selector)
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
return doc.ClickBySelector(values.NewString(selector))
}

View File

@ -0,0 +1,39 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Dispatches click event on all matched element
* @param source (Document) - Document.
* @param selector (String) - Selector.
* @returns (Boolean) - Returns true if matched at least one element.
*/
func ClickAll(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.False, err
}
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
return doc.ClickBySelectorAll(values.NewString(selector))
}

View File

@ -1,132 +0,0 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Returns inner html of a matched element
* @param doc (Document) - Document
* @param selector (String) - Selector
* @returns str (String) - String value of inner html.
*/
func InnerHTML(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.EmptyString, err
}
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.EmptyString, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.InnerHTMLBySelector(values.NewString(selector))
}
/*
* Returns inner html of all matched elements.
* @param doc (Document) - Document
* @param selector (String) - Selector
* @returns array (Array) - Array of string values.
*/
func InnerHTMLAll(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.EmptyString, err
}
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.EmptyString, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.InnerHTMLBySelectorAll(values.NewString(selector))
}
/*
* Returns inner text of a matched element
* @param doc (Document) - Document
* @param selector (String) - Selector
* @returns str (String) - String value of inner text.
*/
func InnerText(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.EmptyString, err
}
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.EmptyString, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.InnerHTMLBySelector(values.NewString(selector))
}
/*
* Returns inner text of all matched elements.
* @param doc (Document) - Document
* @param selector (String) - Selector
* @returns array (Array) - Array of string values.
*/
func InnerTextAll(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.EmptyString, err
}
arg1 := args[0]
selector := args[1].String()
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.EmptyString, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return doc.InnerHTMLBySelectorAll(values.NewString(selector))
}

View File

@ -5,16 +5,38 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/static"
)
func Document(ctx context.Context, inputs ...core.Value) (core.Value, error) {
url, dynamic, err := documentArgs(inputs)
/*
* Loads a document by a given url.
* By default, loads a document by http call - resulted document does not support any interactions.
* If passed "true" as a second argument, headless browser is used for loading the document which support interactions.
* @param url (String) - Target url string. If passed "about:blank" for dynamic document - it will open an empty page.
* @param dynamic (Boolean) - Optional boolean value indicating whether to use dynamic document.
* @returns (HTMLDocument) - Returns loaded HTML document.
*/
func Document(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.None, err
}
err = core.ValidateType(args[0], core.StringType)
url := args[0].(values.String)
dynamic := values.False
if len(args) == 2 {
err = core.ValidateType(args[1], core.BooleanType)
if err != nil {
return values.None, err
}
dynamic = args[1].(values.Boolean)
}
var drv driver.Driver
if !dynamic {
@ -27,56 +49,5 @@ func Document(ctx context.Context, inputs ...core.Value) (core.Value, error) {
return values.None, err
}
return drv.GetDocument(ctx, url.String())
}
func DocumentParse(ctx context.Context, inputs ...core.Value) (core.Value, error) {
arg1 := values.EmptyString
if len(inputs) == 0 {
return arg1, core.Error(core.ErrMissedArgument, "document string")
}
a1 := inputs[0]
if a1.Type() != core.StringType {
return arg1, core.Error(core.TypeError(a1.Type(), core.StringType), "arg 1")
}
drv, err := driver.FromContext(ctx, driver.Static)
if err != nil {
return values.None, err
}
return drv.(*static.Driver).ParseDocument(ctx, arg1.String())
}
func documentArgs(inputs []core.Value) (values.String, values.Boolean, error) {
arg1 := values.EmptyString
arg2 := values.False
if len(inputs) == 0 {
return arg1, arg2, core.Error(core.ErrMissedArgument, "element and useJs")
}
a1 := inputs[0]
if a1.Type() != core.StringType {
return arg1, arg2, core.Error(core.TypeError(a1.Type(), core.StringType), "arg 1")
}
arg1 = a1.(values.String)
if len(inputs) == 2 {
a2 := inputs[1]
if a2.Type() != core.BooleanType {
return arg1, arg2, core.Error(core.TypeError(a2.Type(), core.BooleanType), "arg 2")
}
arg2 = a2.(values.Boolean)
}
return arg1, arg2, nil
return drv.GetDocument(ctx, url)
}

View File

@ -0,0 +1,39 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/static"
)
/*
* Parses a given HTML string and returns a HTML document.
* Returned HTML document is always static.
* @param html (String) - Target HTML string.
* @returns (HTMLDocument) - Parsed HTML static document.
*/
func DocumentParse(ctx context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 1)
if err != nil {
return values.None, err
}
err = core.ValidateType(args[0], core.StringType)
if err != nil {
return values.None, err
}
drv, err := driver.FromContext(ctx, driver.Static)
if err != nil {
return values.None, err
}
str := args[0].(values.String)
return drv.(*static.Driver).ParseDocument(ctx, str)
}

View File

@ -1,5 +1,7 @@
package common
import "strings"
var Attributes = []string{
"abbr",
"accept",
@ -147,7 +149,12 @@ func init() {
}
func IsAttribute(name string) bool {
_, yes := attrMap[name]
_, isDefault := attrMap[name]
return yes
if isDefault {
return true
}
return strings.HasPrefix(name, "data-") ||
strings.HasPrefix(name, "aria-")
}

View File

@ -27,6 +27,10 @@ func NewLazyValue(factory LazyFactory) *LazyValue {
return lz
}
/*
* Indicates whether the value is ready.
* @returns (Boolean) - Boolean value indicating whether the value is ready.
*/
func (lv *LazyValue) Ready() bool {
lv.Lock()
defer lv.Unlock()
@ -34,25 +38,42 @@ func (lv *LazyValue) Ready() bool {
return lv.ready
}
func (lv *LazyValue) Value() (core.Value, error) {
/*
* Returns an underlying value.
* Not thread safe. Should not mutated.
* @returns (Value) - Underlying value if successfully loaded, otherwise error
*/
func (lv *LazyValue) Read() (core.Value, error) {
lv.Lock()
defer lv.Unlock()
if !lv.ready {
val, err := lv.factory()
if err == nil {
lv.value = val
lv.err = nil
} else {
lv.value = values.None
lv.err = err
}
lv.load()
}
return lv.value, lv.err
}
/*
* Safely mutates an underlying value.
* Loads a value if it's not ready.
* Thread safe.
*/
func (lv *LazyValue) Write(writer func(v core.Value, err error)) {
lv.Lock()
defer lv.Unlock()
if !lv.ready {
lv.load()
}
writer(lv.value, lv.err)
}
/*
* Resets the storage.
* Next call of Read will trigger the factory function again.
*/
func (lv *LazyValue) Reset() {
lv.Lock()
defer lv.Unlock()
@ -61,3 +82,17 @@ func (lv *LazyValue) Reset() {
lv.value = values.None
lv.err = nil
}
func (lv *LazyValue) load() {
val, err := lv.factory()
if err == nil {
lv.value = val
lv.err = nil
} else {
lv.value = values.None
lv.err = err
}
lv.ready = true
}

View File

@ -9,23 +9,23 @@ import (
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/static"
)
type DriverName string
type Name string
const (
Dynamic DriverName = "dynamic"
Static DriverName = "static"
Dynamic Name = "dynamic"
Static Name = "static"
)
type Driver interface {
GetDocument(ctx context.Context, url string) (values.HTMLNode, error)
GetDocument(ctx context.Context, url values.String) (values.HTMLNode, error)
Close() error
}
func ToContext(ctx context.Context, name DriverName, drv Driver) context.Context {
func ToContext(ctx context.Context, name Name, drv Driver) context.Context {
return context.WithValue(ctx, name, drv)
}
func FromContext(ctx context.Context, name DriverName) (Driver, error) {
func FromContext(ctx context.Context, name Name) (Driver, error) {
val := ctx.Value(name)
drv, ok := val.(Driver)

View File

@ -21,7 +21,6 @@ import (
"time"
)
const BlankPageURL = "about:blank"
type HTMLDocument struct {
@ -378,7 +377,7 @@ func (doc *HTMLDocument) URL() core.Value {
return doc.url
}
func (doc *HTMLDocument) InnerHTMLBySelector(selector values.String) (values.String, error) {
func (doc *HTMLDocument) InnerHTMLBySelector(selector values.String) values.String {
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
@ -395,17 +394,23 @@ func (doc *HTMLDocument) InnerHTMLBySelector(selector values.String) (values.Str
)
if err != nil {
return values.EmptyString, err
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get inner HTML by selector")
return values.EmptyString
}
if res.Type() == core.StringType {
return res.(values.String), nil
return res.(values.String)
}
return values.EmptyString, nil
return values.EmptyString
}
func (doc *HTMLDocument) InnerHTMLBySelectorAll(selector values.String) (*values.Array, error) {
func (doc *HTMLDocument) InnerHTMLBySelectorAll(selector values.String) *values.Array {
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
@ -427,17 +432,23 @@ func (doc *HTMLDocument) InnerHTMLBySelectorAll(selector values.String) (*values
)
if err != nil {
return values.NewArray(0), err
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get an array of inner HTML by selector")
return values.NewArray(0)
}
if res.Type() == core.ArrayType {
return res.(*values.Array), nil
return res.(*values.Array)
}
return values.NewArray(0), nil
return values.NewArray(0)
}
func (doc *HTMLDocument) InnerTextBySelector(selector values.String) (values.String, error) {
func (doc *HTMLDocument) InnerTextBySelector(selector values.String) values.String {
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
@ -454,17 +465,23 @@ func (doc *HTMLDocument) InnerTextBySelector(selector values.String) (values.Str
)
if err != nil {
return values.EmptyString, err
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get inner text by selector")
return values.EmptyString
}
if res.Type() == core.StringType {
return res.(values.String), nil
return res.(values.String)
}
return values.EmptyString, nil
return values.EmptyString
}
func (doc *HTMLDocument) InnerTextBySelectorAll(selector values.String) (*values.Array, error) {
func (doc *HTMLDocument) InnerTextBySelectorAll(selector values.String) *values.Array {
res, err := eval.Eval(
doc.client,
fmt.Sprintf(`
@ -486,14 +503,20 @@ func (doc *HTMLDocument) InnerTextBySelectorAll(selector values.String) (*values
)
if err != nil {
return values.NewArray(0), err
doc.logger.Error().
Timestamp().
Err(err).
Str("selector", selector.String()).
Msg("failed to get an array inner text by selector")
return values.NewArray(0)
}
if res.Type() == core.ArrayType {
return res.(*values.Array), nil
return res.(*values.Array)
}
return values.NewArray(0), nil
return values.NewArray(0)
}
func (doc *HTMLDocument) ClickBySelector(selector values.String) (values.Boolean, error) {
@ -595,15 +618,16 @@ func (doc *HTMLDocument) InputBySelector(selector values.String, value core.Valu
}
func (doc *HTMLDocument) WaitForSelector(selector values.String, timeout values.Int) error {
task := events.NewWaitTask(
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
el = document.querySelector(%s);
var el = document.querySelector(%s);
if (el != null) {
return true;
}
// null means we need to repeat
return null;
`, eval.ParamString(selector.String())),
time.Millisecond*time.Duration(timeout),
@ -615,6 +639,78 @@ func (doc *HTMLDocument) WaitForSelector(selector values.String, timeout values.
return err
}
func (doc *HTMLDocument) WaitForClass(selector, class values.String, timeout values.Int) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el == null) {
return false;
}
var className = %s;
var found = el.className.split(' ').find(i => i === className);
if (found != null) {
return true;
}
// null means we need to repeat
return null;
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
),
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (doc *HTMLDocument) WaitForClassAll(selector, class values.String, timeout values.Int) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var elements = document.querySelectorAll(%s);
if (elements == null || elements.length === 0) {
return false;
}
var className = %s;
var foundCount = 0;
elements.forEach((el) => {
var found = el.className.split(' ').find(i => i === className);
if (found != null) {
foundCount++;
}
});
if (foundCount === elements.length) {
return true;
}
// null means we need to repeat
return null;
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
),
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (doc *HTMLDocument) WaitForNavigation(timeout values.Int) error {
timer := time.NewTimer(time.Millisecond * time.Duration(timeout))
onEvent := make(chan bool)

View File

@ -28,7 +28,7 @@ func NewDriver(address string) *Driver {
return drv
}
func (drv *Driver) GetDocument(ctx context.Context, url string) (values.HTMLNode, error) {
func (drv *Driver) GetDocument(ctx context.Context, targetURL values.String) (values.HTMLNode, error) {
err := drv.init(ctx)
if err != nil {
@ -38,6 +38,8 @@ func (drv *Driver) GetDocument(ctx context.Context, url string) (values.HTMLNode
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
defer cancel()
url := targetURL.String()
if url == "" {
url = BlankPageURL
}

View File

@ -15,6 +15,7 @@ import (
"github.com/rs/zerolog"
"hash/fnv"
"strconv"
"strings"
"sync"
"time"
)
@ -145,7 +146,7 @@ func (el *HTMLElement) Type() core.Type {
}
func (el *HTMLElement) MarshalJSON() ([]byte, error) {
val, err := el.innerText.Value()
val, err := el.innerText.Read()
if err != nil {
return nil, err
@ -188,7 +189,6 @@ func (el *HTMLElement) Unwrap() interface{} {
return el
}
func (el *HTMLElement) Hash() uint64 {
el.Lock()
defer el.Unlock()
@ -243,7 +243,7 @@ func (el *HTMLElement) NodeName() values.String {
}
func (el *HTMLElement) GetAttributes() core.Value {
val, err := el.attributes.Value()
val, err := el.attributes.Read()
if err != nil {
return values.None
@ -254,7 +254,7 @@ func (el *HTMLElement) GetAttributes() core.Value {
}
func (el *HTMLElement) GetAttribute(name values.String) core.Value {
attrs, err := el.attributes.Value()
attrs, err := el.attributes.Read()
if err != nil {
return values.None
@ -270,7 +270,7 @@ func (el *HTMLElement) GetAttribute(name values.String) core.Value {
}
func (el *HTMLElement) GetChildNodes() core.Value {
val, err := el.loadedChildren.Value()
val, err := el.loadedChildren.Read()
if err != nil {
return values.NewArray(0)
@ -280,7 +280,7 @@ func (el *HTMLElement) GetChildNodes() core.Value {
}
func (el *HTMLElement) GetChildNode(idx values.Int) core.Value {
val, err := el.loadedChildren.Value()
val, err := el.loadedChildren.Read()
if err != nil {
return values.None
@ -365,8 +365,38 @@ func (el *HTMLElement) QuerySelectorAll(selector values.String) core.Value {
return arr
}
func (el *HTMLElement) WaitForClass(class values.String, timeout values.Int) error {
task := events.NewWaitTask(
func() (core.Value, error) {
current := el.GetAttribute("class")
if current.Type() != core.StringType {
return values.None, nil
}
str := current.(values.String)
classStr := string(class)
classes := strings.Split(string(str), " ")
for _, c := range classes {
if c == classStr {
return values.True, nil
}
}
return values.None, nil
},
time.Millisecond*time.Duration(timeout),
events.DefaultPolling,
)
_, err := task.Run()
return err
}
func (el *HTMLElement) InnerText() values.String {
val, err := el.innerText.Value()
val, err := el.innerText.Read()
if err != nil {
return values.EmptyString
@ -469,29 +499,25 @@ func (el *HTMLElement) handleAttrModified(message interface{}) {
return
}
// they are not event loaded
// just ignore the event
if !el.attributes.Ready() {
return
}
el.attributes.Write(func(v core.Value, err error) {
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
val, err := el.attributes.Value()
return
}
// failed to load
if err != nil {
return
}
attrs, ok := v.(*values.Object)
el.Lock()
defer el.Unlock()
if !ok {
return
}
attrs, ok := val.(*values.Object)
if !ok {
return
}
attrs.Set(values.NewString(reply.Name), values.NewString(reply.Value))
attrs.Set(values.NewString(reply.Name), values.NewString(reply.Value))
})
}
func (el *HTMLElement) handleAttrRemoved(message interface{}) {
@ -513,24 +539,25 @@ func (el *HTMLElement) handleAttrRemoved(message interface{}) {
return
}
val, err := el.attributes.Value()
el.attributes.Write(func(v core.Value, err error) {
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
// failed to load
if err != nil {
return
}
return
}
el.Lock()
defer el.Unlock()
attrs, ok := v.(*values.Object)
attrs, ok := val.(*values.Object)
if !ok {
return
}
if !ok {
return
}
// TODO: actually, we need to sync it too...
attrs.Remove(values.NewString(reply.Name))
attrs.Remove(values.NewString(reply.Name))
})
}
func (el *HTMLElement) handleChildrenCountChanged(message interface{}) {
@ -598,40 +625,36 @@ func (el *HTMLElement) handleChildInserted(message interface{}) {
return
}
loaded, err := el.loadedChildren.Value()
el.loadedChildren.Write(func(v core.Value, err error) {
loadedArr := v.(*values.Array)
loadedEl, err := LoadElement(el.logger, el.client, el.broker, nextID)
if err != nil {
return
}
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to load an inserted node")
loadedArr := loaded.(*values.Array)
loadedEl, err := LoadElement(el.logger, el.client, el.broker, nextID)
return
}
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to load an inserted node")
loadedArr.Insert(values.NewInt(targetIDx), loadedEl)
return
}
newInnerHTML, err := loadInnerHTML(el.client, el.id)
loadedArr.Insert(values.NewInt(targetIDx), loadedEl)
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
newInnerHTML, err := loadInnerHTML(el.client, el.id)
return
}
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
return
}
el.innerHTML = newInnerHTML
el.innerHTML = newInnerHTML
})
}
func (el *HTMLElement) handleChildDeleted(message interface{}) {
@ -669,26 +692,32 @@ func (el *HTMLElement) handleChildDeleted(message interface{}) {
return
}
loaded, err := el.loadedChildren.Value()
el.loadedChildren.Write(func(v core.Value, err error) {
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
if err != nil {
return
}
return
}
loadedArr := loaded.(*values.Array)
loadedArr.RemoveAt(values.NewInt(targetIDx))
loadedArr := v.(*values.Array)
loadedArr.RemoveAt(values.NewInt(targetIDx))
newInnerHTML, err := loadInnerHTML(el.client, el.id)
newInnerHTML, err := loadInnerHTML(el.client, el.id)
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
if err != nil {
el.logger.Error().
Timestamp().
Err(err).
Int("id", int(el.id)).
Msg("failed to update node")
return
}
return
}
el.innerHTML = newInnerHTML
el.innerHTML = newInnerHTML
})
}

View File

@ -8,24 +8,24 @@ import (
"time"
)
type WaitTask struct {
client *cdp.Client
predicate string
timeout time.Duration
polling time.Duration
}
type (
Function func() (core.Value, error)
WaitTask struct {
fun Function
timeout time.Duration
polling time.Duration
}
)
const DefaultPolling = time.Millisecond * time.Duration(200)
func NewWaitTask(
client *cdp.Client,
predicate string,
fun Function,
timeout time.Duration,
polling time.Duration,
) *WaitTask {
return &WaitTask{
client,
predicate,
fun,
timeout,
polling,
}
@ -39,14 +39,9 @@ func (task *WaitTask) Run() (core.Value, error) {
case <-timer.C:
return values.None, core.ErrTimeout
default:
out, err := eval.Eval(
task.client,
task.predicate,
true,
false,
)
out, err := task.fun()
// JS expression failed
// expression failed
// terminating
if err != nil {
timer.Stop()
@ -54,7 +49,7 @@ func (task *WaitTask) Run() (core.Value, error) {
return values.None, err
}
// JS output is not empty
// output is not empty
// terminating
if out != values.None {
timer.Stop()
@ -67,3 +62,23 @@ func (task *WaitTask) Run() (core.Value, error) {
}
}
}
func NewEvalWaitTask(
client *cdp.Client,
predicate string,
timeout time.Duration,
polling time.Duration,
) *WaitTask {
return NewWaitTask(
func() (core.Value, error) {
return eval.Eval(
client,
predicate,
true,
false,
)
},
timeout,
polling,
)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/mafredri/cdp/protocol/page"
"github.com/rs/zerolog"
"golang.org/x/sync/errgroup"
"strings"
)
func pointerInt(input int) *int {
@ -34,6 +35,7 @@ func parseAttrs(attrs []string) *values.Object {
res := values.NewObject()
for _, el := range attrs {
el = strings.TrimSpace(el)
str := values.NewString(el)
if common.IsAttribute(el) {
@ -43,7 +45,11 @@ func parseAttrs(attrs []string) *values.Object {
current, ok := res.Get(attr)
if ok {
res.Set(attr, current.(values.String).Concat(values.SpaceString).Concat(str))
if current.String() != "" {
res.Set(attr, current.(values.String).Concat(values.SpaceString).Concat(str))
} else {
res.Set(attr, str)
}
}
}
}

View File

@ -32,16 +32,16 @@ func NewHTMLDocument(
return &HTMLDocument{el, values.NewString(url)}, nil
}
func (el *HTMLDocument) Type() core.Type {
func (doc *HTMLDocument) Type() core.Type {
return core.HTMLDocumentType
}
func (el *HTMLDocument) Compare(other core.Value) int {
func (doc *HTMLDocument) Compare(other core.Value) int {
switch other.Type() {
case core.HTMLDocumentType:
otherDoc := other.(values.HTMLDocument)
return el.url.Compare(otherDoc.URL())
return doc.url.Compare(otherDoc.URL())
default:
if other.Type() > core.HTMLDocumentType {
return -1
@ -51,6 +51,52 @@ func (el *HTMLDocument) Compare(other core.Value) int {
}
}
func (el *HTMLDocument) URL() core.Value {
return el.url
func (doc *HTMLDocument) URL() core.Value {
return doc.url
}
func (doc *HTMLDocument) InnerHTMLBySelector(selector values.String) values.String {
selection := doc.selection.Find(selector.String())
str, err := selection.Html()
// TODO: log error
if err != nil {
return values.EmptyString
}
return values.NewString(str)
}
func (doc *HTMLDocument) InnerHTMLBySelectorAll(selector values.String) *values.Array {
selection := doc.selection.Find(selector.String())
arr := values.NewArray(selection.Length())
selection.Each(func(_ int, selection *goquery.Selection) {
str, err := selection.Html()
// TODO: log error
if err == nil {
arr.Push(values.NewString(str))
}
})
return arr
}
func (doc *HTMLDocument) InnerTextBySelector(selector values.String) values.String {
selection := doc.selection.Find(selector.String())
return values.NewString(selection.Text())
}
func (doc *HTMLDocument) InnerTextBySelectorAll(selector values.String) *values.Array {
selection := doc.selection.Find(selector.String())
arr := values.NewArray(selection.Length())
selection.Each(func(_ int, selection *goquery.Selection) {
arr.Push(values.NewString(selection.Text()))
})
return arr
}

View File

@ -53,7 +53,6 @@ func (el *HTMLElement) Unwrap() interface{} {
return el.selection
}
func (el *HTMLElement) Hash() uint64 {
str, err := el.selection.Html()

View File

@ -297,7 +297,7 @@ func TestElement(t *testing.T) {
So(el.Length(), ShouldEqual, 4)
})
Convey(".Value", t, func() {
Convey(".Read", t, func() {
buff := bytes.NewBuffer([]byte(`
<html>
<head></head>

View File

@ -28,7 +28,8 @@ func NewDriver(setters ...Option) *Driver {
return &Driver{client}
}
func (d *Driver) GetDocument(_ context.Context, url string) (values.HTMLNode, error) {
func (d *Driver) GetDocument(_ context.Context, targetURL values.String) (values.HTMLNode, error) {
url := targetURL.String()
req, err := httpx.NewRequest(httpx.MethodGet, url, nil)
if err != nil {
@ -58,7 +59,7 @@ func (d *Driver) GetDocument(_ context.Context, url string) (values.HTMLNode, er
return NewHTMLDocument(url, doc)
}
func (d *Driver) ParseDocument(_ context.Context, str string) (values.HTMLNode, error) {
func (d *Driver) ParseDocument(_ context.Context, str values.String) (values.HTMLNode, error) {
buf := bytes.NewBuffer([]byte(str))
doc, err := goquery.NewDocumentFromReader(buf)

View File

@ -6,8 +6,15 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values"
)
func Element(_ context.Context, inputs ...core.Value) (core.Value, error) {
el, selector, err := elementArgs(inputs)
/*
* Finds an element by a given CSS selector.
* Returns NONE if element not found.
* @param docOrEl (HTMLDocument|HTMLElement) - Parent document or element.
* @param selector (String) - CSS selector.
* @returns (HTMLElement | None) - Returns an HTMLElement if found, otherwise NONE.
*/
func Element(_ context.Context, args ...core.Value) (core.Value, error) {
el, selector, err := queryArgs(args)
if err != nil {
return values.None, err
@ -16,36 +23,24 @@ func Element(_ context.Context, inputs ...core.Value) (core.Value, error) {
return el.QuerySelector(selector), nil
}
func Elements(_ context.Context, inputs ...core.Value) (core.Value, error) {
el, selector, err := elementArgs(inputs)
func queryArgs(args []core.Value) (values.HTMLNode, values.String, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.None, err
return nil, values.EmptyString, err
}
return el.QuerySelectorAll(selector), nil
}
func elementArgs(inputs []core.Value) (values.HTMLNode, values.String, error) {
if len(inputs) == 0 {
return nil, values.EmptyString, core.Error(core.ErrMissedArgument, "element and arg2")
}
if len(inputs) == 1 {
return nil, values.EmptyString, core.Error(core.ErrMissedArgument, "arg2")
}
arg1 := inputs[0]
arg2 := inputs[1]
if arg1.Type() != core.HTMLDocumentType &&
arg1.Type() != core.HTMLElementType {
return nil, values.EmptyString, core.TypeError(arg1.Type(), core.HTMLDocumentType, core.HTMLElementType)
}
if arg2.Type() != core.StringType {
return nil, values.EmptyString, core.TypeError(arg2.Type(), core.StringType)
}
return arg1.(values.HTMLNode), arg2.(values.String), nil
err = core.ValidateType(args[0], core.HTMLDocumentType, core.HTMLElementType)
if err != nil {
return nil, values.EmptyString, err
}
err = core.ValidateType(args[1], core.StringType)
if err != nil {
return nil, values.EmptyString, err
}
return args[0].(values.HTMLNode), args[1].(values.String), nil
}

View File

@ -0,0 +1,24 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
/*
* Finds HTML elements by a given CSS selector.
* Returns an empty array if element not found.
* @param docOrEl (HTMLDocument|HTMLElement) - Parent document or element.
* @param selector (String) - CSS selector.
* @returns (Array) - Returns an array of found HTML element.
*/
func Elements(_ context.Context, args ...core.Value) (core.Value, error) {
el, selector, err := queryArgs(args)
if err != nil {
return values.None, err
}
return el.QuerySelectorAll(selector), nil
}

View File

@ -1,74 +0,0 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
func WaitElement(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
return values.None, err
}
arg := args[0]
selector := args[1].String()
timeout := values.NewInt(5000)
if len(args) > 2 {
if args[2].Type() == core.IntType {
timeout = args[2].(values.Int)
}
}
err = core.ValidateType(arg, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Error(core.ErrInvalidType, "expected dynamic document")
}
return values.None, doc.WaitForSelector(values.NewString(selector), timeout)
}
func WaitNavigation(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.None, err
}
err = core.ValidateType(args[0], core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := args[0].(*dynamic.HTMLDocument)
if !ok {
return values.None, core.Error(core.ErrInvalidType, "expected dynamic document")
}
timeout := values.NewInt(5000)
if len(args) > 1 {
err = core.ValidateType(args[1], core.IntType)
if err != nil {
return values.None, err
}
timeout = args[1].(values.Int)
}
return values.None, doc.WaitForNavigation(timeout)
}

View File

@ -0,0 +1,38 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
/*
* Returns inner HTML string of a matched element
* @param doc (Document) - Parent document.
* @param selector (String) - String of CSS selector.
* @returns (String) - Inner HTML string if an element found, otherwise NONE.
*/
func InnerHTML(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.EmptyString, 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 := args[0].(values.HTMLDocument)
selector := args[1].(values.String)
return doc.InnerHTMLBySelector(selector), nil
}

View File

@ -0,0 +1,38 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
/*
* Returns an array of inner HTML strings of matched elements.
* @param doc (Document) - Parent document.
* @param selector (String) - String of CSS selector.
* @returns (String) - An array of inner HTML strings if any element found, otherwise empty array.
*/
func InnerHTMLAll(_ 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 := args[0].(values.HTMLDocument)
selector := args[1].(values.String)
return doc.InnerHTMLBySelectorAll(selector), nil
}

View File

@ -0,0 +1,38 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
/*
* Returns inner text of a matched element
* @param doc (Document) - Parent document.
* @param selector (String) - String of CSS selector.
* @returns (String) - Inner text if an element found, otherwise NONE.
*/
func InnerText(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 2)
if err != nil {
return values.EmptyString, 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 := args[0].(values.HTMLDocument)
selector := args[1].(values.String)
return doc.InnerTextBySelector(selector), nil
}

View File

@ -0,0 +1,38 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
/*
* Returns an array of inner text of matched elements.
* @param doc (Document) - Parent document.
* @param selector (String) - String of CSS selector.
* @returns (String) - An array of inner text if any element found, otherwise empty array.
*/
func InnerTextAll(_ 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 := args[0].(values.HTMLDocument)
selector := args[1].(values.String)
return doc.InnerTextBySelectorAll(selector), nil
}

72
pkg/stdlib/html/input.go Normal file
View File

@ -0,0 +1,72 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Sends a value to an underlying input element.
* @param source (Document | Element) - Event target.
* @param valueOrSelector (String) - Selector or a value.
* @param value (String) - Target value.
* @returns (Boolean) - Returns true if an element was found.
*/
func Input(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
return values.None, err
}
// TYPE(el, "foobar")
if len(args) == 2 {
arg1 := args[0]
err := core.ValidateType(arg1, core.HTMLElementType)
if err != nil {
return values.False, err
}
el, ok := arg1.(*dynamic.HTMLElement)
if !ok {
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
err = el.Input(args[1])
if err != nil {
return values.False, err
}
return values.True, nil
}
arg1 := args[0]
err = core.ValidateType(arg1, core.HTMLDocumentType)
if err != nil {
return values.False, err
}
arg2 := args[1]
err = core.ValidateType(arg2, core.StringType)
if err != nil {
return values.False, err
}
doc, ok := arg1.(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
return doc.InputBySelector(arg2.(values.String), args[2])
}

View File

@ -1,6 +1,15 @@
package html
import "github.com/MontFerret/ferret/pkg/runtime/core"
import (
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/pkg/errors"
)
const defaultTimeout = 5000
var (
ErrNotDynamic = errors.New("expected dynamic document or element")
)
func NewLib() map[string]core.Function {
return map[string]core.Function{
@ -10,6 +19,8 @@ func NewLib() map[string]core.Function {
"ELEMENTS": Elements,
"WAIT_ELEMENT": WaitElement,
"WAIT_NAVIGATION": WaitNavigation,
"WAIT_CLASS": WaitClass,
"WAIT_CLASS_ALL": WaitClassAll,
"CLICK": Click,
"CLICK_ALL": ClickAll,
"NAVIGATE": Navigate,

View File

@ -0,0 +1,43 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* 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].(*dynamic.HTMLDocument)
if !ok {
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
return values.None, doc.Navigate(args[1].(values.String))
}

View File

@ -0,0 +1,87 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Waits for a class to appear on a given element.
* Stops the execution until the navigation ends or operation times out.
* @param docOrEl (HTMLDocument|HTMLElement) - Target document or element.
* @param selectorOrClass (String) - If document is passed, this param must represent an element selector.
* Otherwise target class.
* @param classOrTimeout (String|Int, optional) - If document is passed, this param must represent target class name.
* Otherwise timeout.
* @param timeout (Int, optional) - If document is passed, this param must represent timeout.
* Otherwise not passed.
*/
func WaitClass(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 4)
if err != nil {
return values.None, err
}
// document or element
err = core.ValidateType(args[0], core.HTMLDocumentType, core.HTMLElementType)
if err != nil {
return values.None, err
}
// selector or class
err = core.ValidateType(args[1], core.StringType)
if err != nil {
return values.None, err
}
timeout := values.NewInt(defaultTimeout)
// lets figure out what is passed as 1st argument
switch args[0].(type) {
case *dynamic.HTMLDocument:
// class
err = core.ValidateType(args[2], core.StringType)
if err != nil {
return values.None, err
}
doc := args[0].(*dynamic.HTMLDocument)
selector := args[1].(values.String)
class := args[2].(values.String)
if len(args) == 4 {
err = core.ValidateType(args[3], core.IntType)
if err != nil {
return values.None, err
}
timeout = args[3].(values.Int)
}
return values.None, doc.WaitForClass(selector, class, timeout)
case *dynamic.HTMLElement:
el := args[0].(*dynamic.HTMLElement)
class := args[1].(values.String)
if len(args) == 3 {
err = core.ValidateType(args[2], core.IntType)
if err != nil {
return values.None, err
}
timeout = args[3].(values.Int)
}
return values.None, el.WaitForClass(class, timeout)
default:
return values.None, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
}

View File

@ -0,0 +1,67 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Waits for a class to appear on all matched elements.
* Stops the execution until the navigation ends or operation times out.
* @param doc (HTMLDocument) - Parent document.
* @param selector (String) - String of CSS selector.
* @param class (String) - String of target CSS class.
* @param timeout (Int, optional) - Optional timeout.
*/
func WaitClassAll(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 3, 4)
if err != nil {
return values.None, err
}
// document only
err = core.ValidateType(args[0], core.HTMLDocumentType)
if err != nil {
return values.None, err
}
// selector
err = core.ValidateType(args[1], core.StringType)
if err != nil {
return values.None, err
}
// class
err = core.ValidateType(args[2], core.StringType)
if err != nil {
return values.None, err
}
doc, ok := args[0].(*dynamic.HTMLDocument)
if !ok {
return values.None, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
selector := args[1].(values.String)
class := args[2].(values.String)
timeout := values.NewInt(defaultTimeout)
if len(args) == 4 {
err = core.ValidateType(args[3], core.IntType)
if err != nil {
return values.None, err
}
timeout = args[3].(values.Int)
}
return values.None, doc.WaitForClassAll(selector, class, timeout)
}

View File

@ -0,0 +1,51 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Waits for element to appear in the DOM.
* Stops the execution until it finds an element or operation times out.
* @param doc (HTMLDocument) - Dynamic HTMLDocument.
* @param selector (String) - Target element's selector.
* @param timeout (Int, optional) - Optional timeout. Default 5000 ms.
*/
func WaitElement(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
return values.None, err
}
arg := args[0]
selector := args[1].String()
timeout := values.NewInt(defaultTimeout)
if len(args) > 2 {
err = core.ValidateType(args[2], core.IntType)
if err != nil {
return values.None, err
}
timeout = args[2].(values.Int)
}
err = core.ValidateType(arg, core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := arg.(*dynamic.HTMLDocument)
if !ok {
return values.None, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
return values.None, doc.WaitForSelector(values.NewString(selector), timeout)
}

View File

@ -0,0 +1,48 @@
package html
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/stdlib/html/driver/dynamic"
)
/*
* Waits for document to navigate to a new url.
* Stops the execution until the navigation ends or operation times out.
* @param doc (HTMLDocument) - Dynamic HTMLDocument.
* @param timeout (Int, optional) - Optional timeout. Default 5000 ms.
*/
func WaitNavigation(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.None, err
}
err = core.ValidateType(args[0], core.HTMLDocumentType)
if err != nil {
return values.None, err
}
doc, ok := args[0].(*dynamic.HTMLDocument)
if !ok {
return values.None, core.Errors(core.ErrInvalidType, ErrNotDynamic)
}
timeout := values.NewInt(defaultTimeout)
if len(args) > 1 {
err = core.ValidateType(args[1], core.IntType)
if err != nil {
return values.None, err
}
timeout = args[1].(values.Int)
}
return values.None, doc.WaitForNavigation(timeout)
}

View File

@ -7,7 +7,7 @@ import (
)
/*
* Concatenates one or more instances of Value, or an Array.
* Concatenates one or more instances of Read, or an Array.
* @params src (String...|Array) - The source string / array.
* @returns String
*/
@ -42,7 +42,7 @@ func Concat(_ context.Context, args ...core.Value) (core.Value, error) {
}
/*
* Concatenates one or more instances of Value, or an Array with a given separator.
* Concatenates one or more instances of Read, or an Array with a given separator.
* @params separator (string) - The separator string.
* @params src (string...|array) - The source string / array.
* @returns string

View File

@ -10,7 +10,7 @@ import (
/*
* Returns a FQL value described by the JSON-encoded input string.
* @params text (String) - The string to parse as JSON.
* @returns FQL value (Value)
* @returns FQL value (Read)
*/
func JSONParse(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 1)
@ -32,7 +32,7 @@ func JSONParse(_ context.Context, args ...core.Value) (core.Value, error) {
/*
* Returns a JSON string representation of the input value.
* @params value (Value) - The input value to serialize.
* @params value (Read) - The input value to serialize.
* @returns json (String)
*/
func JSONStringify(_ context.Context, args ...core.Value) (core.Value, error) {