1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-03-05 15:16:07 +02:00
This commit is contained in:
Владимир Фетисов 2019-07-03 22:20:56 +03:00
commit 6203b7cd76
199 changed files with 6708 additions and 4132 deletions

5
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: []
patreon: ziflex
open_collective: ferret

3
.github/stale.yml vendored
View File

@ -16,6 +16,7 @@ exemptLabels:
- proposal - proposal
- refactoring - refactoring
- bug - bug
- help wanted
# Set to true to ignore issues in a project (defaults to false) # Set to true to ignore issues in a project (defaults to false)
exemptProjects: false exemptProjects: false
@ -27,7 +28,7 @@ exemptMilestones: true
exemptAssignees: false exemptAssignees: false
# Label to use when marking as stale # Label to use when marking as stale
staleLabel: wontfix staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable # Comment to post when marking as stale. Set to `false` to disable
markComment: > markComment: >

33
.golangci.yml Normal file
View File

@ -0,0 +1,33 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
- pkg/parser/fql
- pkg/parser/antlr
linters:
disable:
- errcheck
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- '^(G104|G401|G505|G501):'
- '^shadow: declaration of'
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
- "*_test.go"

View File

@ -18,6 +18,7 @@ addons:
install: install:
- go get -u github.com/mgechev/revive - go get -u github.com/mgechev/revive
- go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
- sudo curl -o /usr/local/lib/antlr-4.7.1-complete.jar https://www.antlr.org/download/antlr-4.7.1-complete.jar - sudo curl -o /usr/local/lib/antlr-4.7.1-complete.jar https://www.antlr.org/download/antlr-4.7.1-complete.jar
- export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" - export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH"
- mkdir $HOME/travis-bin - mkdir $HOME/travis-bin

View File

@ -30,7 +30,7 @@ cover:
curl -s https://codecov.io/bash | bash curl -s https://codecov.io/bash | bash
e2e: e2e:
go run ${DIR_E2E}/main.go --tests ${DIR_E2E}/tests --pages ${DIR_E2E}/pages --filter doc_cookie_set* go run ${DIR_E2E}/main.go --tests ${DIR_E2E}/tests --pages ${DIR_E2E}/pages
bench: bench:
go test -run=XXX -bench=. ${DIR_PKG}/... go test -run=XXX -bench=. ${DIR_PKG}/...
@ -48,7 +48,8 @@ fmt:
# https://github.com/mgechev/revive # https://github.com/mgechev/revive
# go get github.com/mgechev/revive # go get github.com/mgechev/revive
lint: lint:
revive -config revive.toml -formatter friendly -exclude ./pkg/parser/fql/... -exclude ./vendor/... ./... revive -config revive.toml -formatter friendly -exclude ./pkg/parser/fql/... -exclude ./vendor/... ./... && \
golangci-lint run ./pkg/...
# http://godoc.org/code.google.com/p/go.tools/cmd/vet # http://godoc.org/code.google.com/p/go.tools/cmd/vet
# go get code.google.com/p/go.tools/cmd/vet # go get code.google.com/p/go.tools/cmd/vet

View File

@ -532,12 +532,26 @@ LET doc = DOCUMENT("https://www.google.com", {
] ]
}) })
COOKIES_SET(doc, { name: "baz", value: "qaz"}, { name: "daz", value: "gag" }) COOKIE_SET(doc, { name: "baz", value: "qaz"}, { name: "daz", value: "gag" })
COOKIES_DEL(doc, "foo") COOKIE_DEL(doc, "foo")
LET c = COOKIES_GET(doc, "baz") LET c = COOKIE_GET(doc, "baz")
FOR cookie IN doc.cookies FOR cookie IN doc.cookies
RETURN cookie.name RETURN cookie.name
``` ```
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website.
<p>
<a href="https://opencollective.com/ferret/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ferret/sponsor/0/avatar.svg"></a>
</p>
<p align="center">
<a href="https://opencollective.com/ferret/donate" target="_blank">
<img src="https://opencollective.com/ferret/donate/button@2x.png?color=blue" width="300" />
</a>
</p>

View File

@ -62,7 +62,7 @@ func (b *Browser) Close() error {
return nil return nil
} }
os.RemoveAll(tmpDir) err = os.RemoveAll(tmpDir)
if err != nil { if err != nil {
return err return err

View File

@ -47,7 +47,7 @@ func Launch(setters ...Option) (*Browser, error) {
temporaryUserDataDir := opts.userDataDir temporaryUserDataDir := opts.userDataDir
if temporaryUserDataDir == "" && opts.noUserDataDir == false { if temporaryUserDataDir == "" && !opts.noUserDataDir {
dirName, err := ioutil.TempDir(os.TempDir(), "ferret_dev_profile-") dirName, err := ioutil.TempDir(os.TempDir(), "ferret_dev_profile-")
if err != nil { if err != nil {

View File

@ -4,13 +4,15 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"github.com/MontFerret/ferret/e2e/runner" "net"
"github.com/MontFerret/ferret/e2e/server"
"github.com/rs/zerolog"
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"regexp"
"github.com/MontFerret/ferret/e2e/runner"
"github.com/MontFerret/ferret/e2e/server"
"github.com/rs/zerolog"
) )
var ( var (
@ -39,6 +41,20 @@ var (
) )
) )
func getOutboundIP() (net.IP, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}
func main() { func main() {
flag.Parse() flag.Parse()
@ -56,19 +72,6 @@ func main() {
Dir: filepath.Join(*pagesDir, "dynamic"), Dir: filepath.Join(*pagesDir, "dynamic"),
}) })
var filterR *regexp.Regexp
if *filter != "" {
r, err := regexp.Compile(*filter)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
filterR = r
}
go func() { go func() {
if err := static.Start(); err != nil { if err := static.Start(); err != nil {
logger.Info().Timestamp().Msg("shutting down the static pages server") logger.Info().Timestamp().Msg("shutting down the static pages server")
@ -81,26 +84,35 @@ func main() {
} }
}() }()
dirname := *testsDir if *testsDir == "" {
_, err := filepath.Abs(filepath.Dir(os.Args[0]))
if dirname == "" {
d, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil { if err != nil {
logger.Fatal().Timestamp().Err(err).Msg("failed to get testsDir") logger.Fatal().Timestamp().Err(err).Msg("failed to get testsDir")
return return
} }
}
dirname = d var ipAddr string
// we need it in those cases when a Chrome instance is running inside a container
// and it needs an external IP to get access to our static web server
outIP, err := getOutboundIP()
if err != nil {
ipAddr = "0.0.0.0"
logger.Warn().Err(err).Msg("Failed to get outbound IP address")
} else {
ipAddr = outIP.String()
} }
r := runner.New(logger, runner.Settings{ r := runner.New(logger, runner.Settings{
StaticServerAddress: fmt.Sprintf("http://0.0.0.0:%d", staticPort), StaticServerAddress: fmt.Sprintf("http://%s:%d", ipAddr, staticPort),
DynamicServerAddress: fmt.Sprintf("http://0.0.0.0:%d", dynamicPort), DynamicServerAddress: fmt.Sprintf("http://%s:%d", ipAddr, dynamicPort),
CDPAddress: *cdp, CDPAddress: *cdp,
Dir: *testsDir, Dir: *testsDir,
Filter: filterR, Filter: *filter,
}) })
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -114,7 +126,7 @@ func main() {
} }
}() }()
err := r.Run(ctx) err = r.Run(ctx)
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)

View File

@ -2,6 +2,7 @@ import Layout from './layout.js';
import IndexPage from './pages/index.js'; import IndexPage from './pages/index.js';
import FormsPage from './pages/forms/index.js'; import FormsPage from './pages/forms/index.js';
import EventsPage from './pages/events/index.js'; import EventsPage from './pages/events/index.js';
import IframePage from './pages/iframes/index.js';
const e = React.createElement; const e = React.createElement;
const Router = ReactRouter.Router; const Router = ReactRouter.Router;
@ -10,7 +11,26 @@ const Route = ReactRouter.Route;
const Redirect = ReactRouter.Redirect; const Redirect = ReactRouter.Redirect;
const createBrowserHistory = History.createBrowserHistory; const createBrowserHistory = History.createBrowserHistory;
export default function AppComponent({ redirect = null}) { export default React.memo(function AppComponent(params = {}) {
let redirectTo;
if (params.redirect) {
let search = '';
Object.keys(params).forEach((key) => {
if (key !== 'redirect') {
search += `${key}=${params[key]}`;
}
});
const to = {
pathname: params.redirect,
search: search ? `?${search}` : '',
};
redirectTo = e(Redirect, { to });
}
return e(Router, { history: createBrowserHistory() }, return e(Router, { history: createBrowserHistory() },
e(Layout, null, [ e(Layout, null, [
e(Switch, null, [ e(Switch, null, [
@ -27,8 +47,12 @@ export default function AppComponent({ redirect = null}) {
path: '/events', path: '/events',
component: EventsPage component: EventsPage
}), }),
e(Route, {
path: '/iframe',
component: IframePage
}),
]), ]),
redirect ? e(Redirect, { to: redirect }) : null redirectTo
]) ])
) )
} })

View File

@ -18,6 +18,9 @@ export default function Layout({ children }) {
]), ]),
e("li", { className: "nav-item"}, [ e("li", { className: "nav-item"}, [
e(NavLink, { className: "nav-link", to: "/events" }, "Events") e(NavLink, { className: "nav-link", to: "/events" }, "Events")
]),
e("li", { className: "nav-item"}, [
e(NavLink, { className: "nav-link", to: "/iframe" }, "iFrame")
]) ])
]) ])
]) ])

View File

@ -0,0 +1,26 @@
import { parse } from '../../../utils/qs.js';
const e = React.createElement;
export default class IFramePage extends React.Component {
render() {
const search = parse(this.props.location.search);
let redirect;
if (search.src) {
redirect = search.src;
}
return e("div", { id: "iframe" }, [
e("iframe", {
name: 'nested',
style: {
width: '100%',
height: '800px',
},
src: redirect ? `/?redirect=${redirect}` : '/'
}),
])
}
}

View File

@ -11,9 +11,9 @@
</head> </head>
<body class="text-center"> <body class="text-center">
<div id="root"></div> <div id="root"></div>
<script src="https://unpkg.com/react@16.6.1/umd/react.production.min.js"></script> <script src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.6.1/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/history@4.7.2/umd/history.min.js"></script> <script src="https://unpkg.com/history@4.9.0/umd/history.min.js"></script>
<script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script> <script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script>
<script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script> <script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script>
<script src="index.js" type="module"></script> <script src="index.js" type="module"></script>

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,8 @@ package runner
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"net/http"
"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"
@ -11,6 +13,8 @@ import (
func Assertions() map[string]core.Function { func Assertions() map[string]core.Function {
return map[string]core.Function{ return map[string]core.Function{
"EXPECT": expect, "EXPECT": expect,
"T::EXPECT": expect,
"T::HTTP::GET": httpGet,
} }
} }
@ -27,3 +31,29 @@ func expect(_ context.Context, args ...core.Value) (core.Value, error) {
return values.NewString(fmt.Sprintf(`expected "%s", but got "%s"`, args[0], args[1])), nil return values.NewString(fmt.Sprintf(`expected "%s", but got "%s"`, args[0], args[1])), nil
} }
func httpGet(_ context.Context, args ...core.Value) (core.Value, error) {
err := core.ValidateArgs(args, 1, 2)
if err != nil {
return values.None, err
}
url := args[0].String()
resp, err := http.Get(url)
if err != nil {
return values.None, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return values.None, err
}
return values.String(b), nil
}

View File

@ -6,7 +6,6 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"time" "time"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
@ -14,6 +13,8 @@ import (
"github.com/MontFerret/ferret/pkg/drivers/cdp" "github.com/MontFerret/ferret/pkg/drivers/cdp"
"github.com/MontFerret/ferret/pkg/drivers/http" "github.com/MontFerret/ferret/pkg/drivers/http"
"github.com/MontFerret/ferret/pkg/runtime" "github.com/MontFerret/ferret/pkg/runtime"
"github.com/gobwas/glob"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -24,7 +25,7 @@ type (
DynamicServerAddress string DynamicServerAddress string
CDPAddress string CDPAddress string
Dir string Dir string
Filter *regexp.Regexp Filter string
} }
Result struct { Result struct {
@ -84,7 +85,7 @@ func (r *Runner) Run(ctx context.Context) error {
Timestamp(). Timestamp().
Int("passed", sum.passed). Int("passed", sum.passed).
Int("failed", sum.failed). Int("failed", sum.failed).
Dur("time", sum.duration). Str("duration", sum.duration.String()).
Msg("Completed") Msg("Completed")
if sum.failed > 0 { if sum.failed > 0 {
@ -95,19 +96,7 @@ func (r *Runner) Run(ctx context.Context) error {
} }
func (r *Runner) runQueries(ctx context.Context, dir string) ([]Result, error) { func (r *Runner) runQueries(ctx context.Context, dir string) ([]Result, error) {
files, err := ioutil.ReadDir(dir) results := make([]Result, 0, 50)
if err != nil {
r.logger.Error().
Timestamp().
Err(err).
Str("dir", dir).
Msg("failed to read scripts directory")
return nil, err
}
results := make([]Result, 0, len(files))
c := compiler.New() c := compiler.New()
@ -115,48 +104,71 @@ func (r *Runner) runQueries(ctx context.Context, dir string) ([]Result, error) {
return nil, err return nil, err
} }
// read scripts var filter glob.Glob
for _, f := range files { var useFilter bool
n := f.Name()
if r.settings.Filter != nil { if r.settings.Filter != "" {
if r.settings.Filter.Match([]byte(n)) != true { f, err := glob.Compile(r.settings.Filter)
continue
if err != nil {
return nil, err
}
filter = f
useFilter = true
}
err := r.traverseDir(ctx, dir, func(name string) error {
if useFilter {
if !filter.Match(name) {
return nil
} }
} }
fName := filepath.Join(dir, n) b, err := ioutil.ReadFile(name)
b, err := ioutil.ReadFile(fName)
if err != nil { if err != nil {
results = append(results, Result{ results = append(results, Result{
name: fName, name: name,
err: errors.Wrap(err, "failed to read script file"), err: errors.Wrap(err, "failed to read script file"),
}) })
continue return nil
} }
r.logger.Info().Timestamp().Str("name", fName).Msg("Running test") r.logger.Info().Timestamp().Str("name", name).Msg("Running test")
result := r.runQuery(ctx, c, fName, string(b)) select {
case <-ctx.Done():
return context.Canceled
default:
result := r.runQuery(ctx, c, name, string(b))
if result.err == nil { if result.err == nil {
r.logger.Info(). r.logger.Info().
Timestamp(). Timestamp().
Str("file", result.name). Str("file", result.name).
Str("duration", result.duration.String()).
Msg("Test passed") Msg("Test passed")
} else { } else {
r.logger.Error(). r.logger.Error().
Timestamp(). Timestamp().
Err(result.err). Err(result.err).
Str("file", result.name). Str("file", result.name).
Str("duration", result.duration.String()).
Msg("Test failed") Msg("Test failed")
} }
results = append(results, result) results = append(results, result)
} }
return nil
})
if err != nil {
return nil, err
}
return results, nil return results, nil
} }
@ -180,7 +192,7 @@ func (r *Runner) runQuery(ctx context.Context, c *compiler.FqlCompiler, name, sc
runtime.WithParam("dynamic", r.settings.DynamicServerAddress), runtime.WithParam("dynamic", r.settings.DynamicServerAddress),
) )
duration := time.Now().Sub(start) duration := time.Since(start)
if err != nil { if err != nil {
return Result{ return Result{
@ -237,3 +249,35 @@ func (r *Runner) report(results []Result) Summary {
duration: sumDuration, duration: sumDuration,
} }
} }
func (r *Runner) traverseDir(ctx context.Context, dir string, iteratee func(name string) error) error {
files, err := ioutil.ReadDir(dir)
if err != nil {
r.logger.Error().
Timestamp().
Err(err).
Str("dir", dir).
Msg("failed to read scripts directory")
return err
}
for _, file := range files {
name := filepath.Join(dir, file.Name())
if file.IsDir() {
if err := r.traverseDir(ctx, name, iteratee); err != nil {
return err
}
continue
}
if err := iteratee(name); err != nil {
return err
}
}
return nil
}

View File

@ -2,10 +2,13 @@ package server
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/labstack/echo"
"net/http" "net/http"
"path/filepath" "path/filepath"
"time"
"github.com/labstack/echo"
) )
type ( type (
@ -37,6 +40,43 @@ func New(settings Settings) *Server {
}) })
e.Static("/", settings.Dir) e.Static("/", settings.Dir)
e.File("/", filepath.Join(settings.Dir, "index.html")) e.File("/", filepath.Join(settings.Dir, "index.html"))
api := e.Group("/api")
api.GET("/ts", func(ctx echo.Context) error {
var headers string
if len(ctx.Request().Header) > 0 {
b, err := json.Marshal(ctx.Request().Header)
if err != nil {
return err
}
headers = string(b)
}
ts := time.Now().Format("2006-01-02 15:04:05")
return ctx.HTML(http.StatusOK, fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<span id="timestamp">%s</span>
<span id="headers">%s</span>
</body>
</html>
`, ts, headers))
})
api.GET("/ping", func(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, echo.Map{
"header": ctx.Request().Header,
"url": ctx.Request().URL,
"data": "pong",
"ts": time.Now(),
})
})
return &Server{e, settings} return &Server{e, settings}
} }

View File

@ -1,7 +1,7 @@
LET url = @dynamic LET url = @dynamic
LET doc = DOCUMENT(url, true) LET doc = DOCUMENT(url, true)
LET expected = `<!DOCTYPE html><html lang="en"><head> LET expected = `<head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge">
<title>Ferret E2E SPA</title> <title>Ferret E2E SPA</title>
@ -11,16 +11,16 @@ LET expected = `<!DOCTYPE html><html lang="en"><head>
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css">
</head> </head>
<body class="text-center"> <body class="text-center">
<div id="root"><div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="/events">Events</a></li></ul></div></nav><main class="container"><div class="jumbotron" data-type="page" id="index"><div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div></div></main></div></div> <div id="root"><div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="/events">Events</a></li><li class="nav-item"><a class="nav-link" href="/iframe">iFrame</a></li></ul></div></nav><main class="container"><div class="jumbotron" data-type="page" id="index"><div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div></div></main></div></div>
<script src="https://unpkg.com/react@16.6.1/umd/react.production.min.js"></script> <script src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@16.6.1/umd/react-dom.production.min.js"></script> <script src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/history@4.7.2/umd/history.min.js"></script> <script src="https://unpkg.com/history@4.9.0/umd/history.min.js"></script>
<script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script> <script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script>
<script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script> <script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script>
<script src="index.js" type="module"></script> <script src="index.js" type="module"></script>
</body></html>` </body>`
LET actual = INNER_HTML(doc) LET actual = INNER_HTML(doc)
LET r1 = '(\s|\")' LET r1 = '(\s|\")'

View File

@ -1,11 +1,11 @@
LET url = @dynamic LET url = @dynamic
LET doc = DOCUMENT(url, true) LET doc = DOCUMENT(url, true)
LET expected = `Ferret E2E SPA LET expected = `Ferret
Ferret
Forms Forms
Navigation Navigation
Events Events
iFrame
Welcome to Ferret E2E test page! Welcome to Ferret E2E test page!
It has several pages for testing different possibilities of the library It has several pages for testing different possibilities of the library
` `

View File

@ -0,0 +1,12 @@
LET url = @dynamic + "?redirect=/iframe"
LET page = DOCUMENT(url, { driver: 'cdp' })
LET doc = page.frames[1]
LET expectedP = TRUE
LET actualP = ELEMENT_EXISTS(doc, '.text-center')
LET expectedN = FALSE
LET actualN = ELEMENT_EXISTS(doc, '.foo-bar')
RETURN EXPECT(expectedP + expectedN, actualP + expectedN)

View File

@ -0,0 +1,12 @@
LET url = @dynamic + "?redirect=/iframe&src=/events"
LET page = DOCUMENT(url, { driver: 'cdp' })
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "#page-events")
HOVER(doc, "#hoverable-btn")
WAIT_ELEMENT(doc, "#hoverable-content")
LET output = INNER_TEXT(doc, "#hoverable-content")
RETURN EXPECT(output, "Lorem ipsum dolor sit amet.")

View File

@ -0,0 +1,11 @@
LET url = @dynamic + "?redirect=/iframe&src=/forms"
LET page = DOCUMENT(url, true)
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "form")
LET output = ELEMENT(doc, "#text_output")
INPUT(doc, "#text_input", "foo")
RETURN EXPECT(output.innerText, "foo")

View File

@ -0,0 +1,4 @@
LET url = @dynamic + "?redirect=/iframe"
LET doc = DOCUMENT(url, { driver: 'cdp' })
RETURN EXPECT(2, LENGTH(doc.frames))

View File

@ -0,0 +1,15 @@
LET url = @dynamic + "?redirect=/iframe&src=/events"
LET page = DOCUMENT(url, true)
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "#page-events")
// with fixed timeout
CLICK(doc, "#wait-class-btn")
WAIT_CLASS(doc, "#wait-class-content", "alert-success")
// with random timeout
CLICK(doc, "#wait-class-random-btn")
WAIT_CLASS(doc, "#wait-class-random-content", "alert-success", 10000)
RETURN ""

View File

@ -0,0 +1,6 @@
LET url = @dynamic
LET page = DOCUMENT(url, true)
LET actual = XPATH(page, "count(//body)")
RETURN EXPECT(1, actual)

View File

@ -0,0 +1,6 @@
LET url = @dynamic + "?redirect=/forms"
LET page = DOCUMENT(url, true)
LET actual = XPATH(page, "//div[contains(@class, 'form-group')]")
RETURN EXPECT(4, LENGTH(actual))

View File

@ -0,0 +1,14 @@
LET url = @dynamic + "?redirect=/iframe&src=/events"
LET page = DOCUMENT(url, { driver: 'cdp' })
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "#page-events")
LET input = ELEMENT(doc, "#hoverable-btn")
HOVER(input)
WAIT_ELEMENT(doc, "#hoverable-content")
LET output = ELEMENT(doc, "#hoverable-content")
RETURN EXPECT(output.innerText, "Lorem ipsum dolor sit amet.")

View File

@ -0,0 +1,12 @@
LET url = @dynamic + "?redirect=/iframe&src=/forms"
LET page = DOCUMENT(url, { driver: 'cdp' })
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "form")
LET input = ELEMENT(doc, "#text_input")
LET output = ELEMENT(doc, "#text_output")
INPUT(input, "foo")
RETURN EXPECT(output.innerText, "foo")

View File

@ -0,0 +1,11 @@
LET url = @dynamic + "?redirect=/iframe&src=/forms"
LET page = DOCUMENT(url, { driver: 'cdp' })
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "form")
LET input = ELEMENT(doc, "#select_input")
LET output = ELEMENT(doc, "#select_output")
LET result = SELECT(input, ["4"])
RETURN EXPECT(output.innerText, "4") + EXPECT(JSON_STRINGIFY(result), '["4"]')

View File

@ -0,0 +1,23 @@
LET url = @dynamic + "?redirect=/iframe&src=/events"
LET page = DOCUMENT(url, { driver: 'cdp' })
LET doc = page.frames[1]
WAIT_ELEMENT(doc, "#page-events")
// with fixed timeout
LET b1 = ELEMENT(doc, "#wait-class-btn")
LET c1 = ELEMENT(doc, "#wait-class-content")
WAIT(2000)
CLICK(b1)
WAIT_CLASS(c1, "alert-success")
// with random timeout
LET b2 = ELEMENT(doc, "#wait-class-random-btn")
LET c2 = ELEMENT(doc, "#wait-class-random-content")
CLICK(b2)
WAIT_CLASS(c2, "alert-success", 10000)
RETURN ""

View File

@ -0,0 +1,7 @@
LET url = @dynamic
LET page = DOCUMENT(url, true)
LET el = ELEMENT(page, 'main')
LET actual = XPATH(el, "count(//p)")
RETURN EXPECT(1, actual)

View File

@ -0,0 +1,7 @@
LET url = @dynamic + "?redirect=/forms"
LET page = DOCUMENT(url, true)
LET element = ELEMENT(page, '#page-form')
LET actual = XPATH(element, "//div[contains(@class, 'form-group')]")
RETURN EXPECT(4, LENGTH(actual))

View File

@ -2,7 +2,7 @@ LET url = @dynamic
LET doc = DOCUMENT(url, true) LET doc = DOCUMENT(url, true)
LET el = ELEMENT(doc, "#root") LET el = ELEMENT(doc, "#root")
LET expected = `<div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="/events">Events</a></li></ul></div></nav><main class="container"><div class="jumbotron" data-type="page" id="index"><div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div></div></main></div>` LET expected = `<div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="/events">Events</a></li><li class="nav-item"><a class="nav-link" href="/iframe">iFrame</a></li></ul></div></nav><main class="container"><div class="jumbotron" data-type="page" id="index"><div><h1>Welcome to Ferret E2E test page!</h1></div><div><p class="lead">It has several pages for testing different possibilities of the library</p></div></div></main></div>`
LET actual = INNER_HTML(el) LET actual = INNER_HTML(el)
LET r1 = '(\s|\")' LET r1 = '(\s|\")'

View File

@ -1,12 +1,9 @@
LET url = @static + '/value.html' LET url = @dynamic + "?redirect=/forms"
LET doc = DOCUMENT(url, true) LET doc = DOCUMENT(url, true)
LET expected = ["068728","068728","816410","52024413","698690","210583","049700","826394","354369","135911","700285","557242","278832","357701","313034","959368","703500","842750","777175","378061","072489","383005","843393","59912263","464535","229710","230550","767964","758862","944384","025449","010245","844935","038760","013450","124139","211145","758761","448667","488966"] LET el = ELEMENT(doc, "#select_input")
LET actual = ( LET expected = "1"
FOR tr IN ELEMENTS(doc, '#listings_table > tbody > tr') LET actual = el.value
LET elem = ELEMENT(tr, 'td > input')
RETURN elem.value
)
RETURN EXPECT(actual, expected) RETURN EXPECT(actual, expected)

View File

@ -1,4 +1,7 @@
LET url = @dynamic LET url = @dynamic
LET doc = DOCUMENT(url, true) LET doc = DOCUMENT(url, true)
RETURN EXPECT(doc.url, url) LET expected = url + '/'
LET actual = doc.url
RETURN EXPECT(expected, actual)

View File

@ -0,0 +1,10 @@
LET url = @static + '/api/ts'
LET ua = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) brave/0.7.10 Chrome/47.0.2526.110 Brave/0.36.5 Safari/537.36"
LET page = DOCUMENT(url, {
userAgent: ua
})
LET h = ELEMENT(page, "#headers")
LET headers = JSON_PARSE(h.innerText)
RETURN T::EXPECT(ua, headers["User-Agent"][0])

View File

@ -0,0 +1,6 @@
LET url = @static + '/overview.html'
LET page = DOCUMENT(url)
LET actual = XPATH(page, "count(//body)")
RETURN EXPECT(1, actual)

View File

@ -0,0 +1,6 @@
LET url = @static + '/value.html'
LET page = DOCUMENT(url)
LET actual = XPATH(page, "//tr[contains(@class, 'odd')]")
RETURN EXPECT(20, LENGTH(actual))

View File

@ -0,0 +1,7 @@
LET url = @static + '/value.html'
LET page = DOCUMENT(url)
LET el = ELEMENT(page, '#listings_table')
LET actual = XPATH(el, "count(//tr)")
RETURN EXPECT(41, actual)

View File

@ -0,0 +1,7 @@
LET url = @static + '/value.html'
LET page = DOCUMENT(url, true)
LET element = ELEMENT(page, '.tablesorter')
LET actual = XPATH(element, "//input[contains(@type, 'hidden')]")
RETURN EXPECT(40, LENGTH(actual))

View File

@ -2,8 +2,6 @@ package main
import ( import (
"context" "context"
"fmt"
"os"
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers"

View File

@ -10,6 +10,7 @@ import (
"github.com/MontFerret/ferret/pkg/compiler" "github.com/MontFerret/ferret/pkg/compiler"
"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/MontFerret/ferret/pkg/runtime/values/types"
) )
func main() { func main() {
@ -37,7 +38,7 @@ func getStrings() ([]string, error) {
} }
// this is another helper functions allowing to do type validation // this is another helper functions allowing to do type validation
err = core.ValidateType(args[0], core.StringType) err = core.ValidateType(args[0], types.String)
if err != nil { if err != nil {
return values.None, err return values.None, err

5
examples/iframes.fql Normal file
View File

@ -0,0 +1,5 @@
LET page = DOCUMENT("https://www.w3schools.com/html/html_iframe.asp", { driver: "cdp" })
LET c2 = page.frames[1].head.innerHTML
RETURN c2

View File

@ -4,14 +4,15 @@ INPUT(amazon, '#twotabsearchtextbox', @criteria)
CLICK(amazon, '.nav-search-submit input[type="submit"]') CLICK(amazon, '.nav-search-submit input[type="submit"]')
WAIT_NAVIGATION(amazon) WAIT_NAVIGATION(amazon)
LET resultListSelector = '#s-results-list-atf' LET resultListSelector = 'div.s-result-list'
LET resultItemSelector = '.s-result-item.celwidget' LET resultItemSelector = 'div.s-result-item'
LET nextBtnSelector = '#pagnNextLink' LET nextBtnSelector = 'ul.a-pagination .a-last a'
LET vendorSelector1 = 'div > div:nth-child(3) > div:nth-child(2) > span:nth-child(2)' LET vendorSelector1 = 'div > div:nth-child(3) > div:nth-child(2) > span:nth-child(2)'
LET vendorSelector2 = 'div > div:nth-child(5) > div:nth-child(2) > span:nth-child(2)' LET vendorSelector2 = 'div > div:nth-child(5) > div:nth-child(2) > span:nth-child(2)'
LET priceWholeSelector = 'span.sx-price-whole' LET priceWholeSelector = 'span.sx-price-whole'
LET priceFracSelector = 'sup.sx-price-fractional' LET priceFracSelector = 'sup.sx-price-fractional'
LET pages = TO_INT(INNER_TEXT(amazon, '#pagn > span.pagnDisabled')) LET pagers = ELEMENTS(amazon, 'ul.a-pagination li.a-disabled')
LET pages = LENGTH(pagers) > 0 ? TO_INT(INNER_TEXT(LAST(pagers))) : 0
LET result = ( LET result = (
FOR pageNum IN 1..pages FOR pageNum IN 1..pages
@ -19,6 +20,8 @@ LET result = (
LET wait = clicked ? WAIT_NAVIGATION(amazon) : false LET wait = clicked ? WAIT_NAVIGATION(amazon) : false
LET waitSelector = wait ? WAIT_ELEMENT(amazon, resultListSelector) : false LET waitSelector = wait ? WAIT_ELEMENT(amazon, resultListSelector) : false
PRINT("page:", pageNum, "clicked", clicked)
LET items = ( LET items = (
FOR el IN ELEMENTS(amazon, resultItemSelector) FOR el IN ELEMENTS(amazon, resultItemSelector)
LET priceWholeTxt = INNER_TEXT(el, priceWholeSelector) LET priceWholeTxt = INNER_TEXT(el, priceWholeSelector)

3
examples/screenshot.fql Normal file
View File

@ -0,0 +1,3 @@
LET data = SCREENSHOT("https://github.com/MontFerret/ferret/raw/master/assets/logo.png")
RETURN { type: "png", data }

60
go.mod
View File

@ -3,74 +3,42 @@ module github.com/MontFerret/ferret
go 1.12 go 1.12
require ( require (
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810 // indirect github.com/BurntSushi/toml v0.3.1 // indirect
github.com/PuerkitoBio/goquery v1.5.0 github.com/PuerkitoBio/goquery v1.5.0
github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 // indirect github.com/antchfx/htmlquery v1.0.0
github.com/antchfx/xpath v1.0.0
github.com/antlr/antlr4 v0.0.0-20190325153624-837aa60e2c47 github.com/antlr/antlr4 v0.0.0-20190325153624-837aa60e2c47
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/coreos/etcd v3.3.12+incompatible // indirect github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/corpix/uarand v0.0.0 github.com/corpix/uarand v0.0.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9 github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9
github.com/fatih/color v1.7.0 // indirect github.com/gobwas/glob v0.2.3
github.com/go-critic/go-critic v0.3.4 // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/go-toolsmith/astcast v1.0.0 // indirect
github.com/go-toolsmith/astcopy v1.0.0 // indirect
github.com/go-toolsmith/astfmt v1.0.0 // indirect
github.com/go-toolsmith/astp v1.0.0 // indirect
github.com/go-toolsmith/pkgload v1.0.0 // indirect
github.com/go-toolsmith/typep v1.0.0 // indirect
github.com/gofrs/uuid v3.2.0+incompatible github.com/gofrs/uuid v3.2.0+incompatible
github.com/gogo/protobuf v1.2.1 // indirect github.com/google/go-cmp v0.2.0 // indirect
github.com/golang/mock v1.2.0 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0 // indirect
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 // indirect
github.com/golangci/golangci-lint v1.15.0 // indirect
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb // indirect
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219 // indirect
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f // indirect github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f // indirect
github.com/gorilla/css v1.0.0 github.com/gorilla/css v1.0.0
github.com/gorilla/websocket v1.4.0 // indirect github.com/gorilla/websocket v1.4.0 // indirect
github.com/kisielk/errcheck v1.2.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/kr/pty v1.1.4 // indirect
github.com/labstack/echo v3.3.10+incompatible github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.2.8 // indirect github.com/labstack/gommon v0.2.8 // indirect
github.com/mafredri/cdp v0.23.1 github.com/mafredri/cdp v0.23.4
github.com/mattn/go-colorable v0.1.1 // indirect github.com/mattn/go-colorable v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect github.com/mattn/go-isatty v0.0.7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mozilla/tls-observatory v0.0.0-20190313211306-43961c0c7a1f // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
github.com/onsi/ginkgo v1.8.0 // indirect
github.com/onsi/gomega v1.5.0 // indirect
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.8.1
github.com/rogpeppe/go-internal v1.2.2 // indirect
github.com/rs/zerolog v1.14.3 github.com/rs/zerolog v1.14.3
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0
github.com/shirou/gopsutil v2.18.12+incompatible // indirect
github.com/shurcooL/go v0.0.0-20190121191506-3fef8c783dec // indirect
github.com/sirupsen/logrus v1.4.0 // indirect
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac // indirect github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac // indirect
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff
github.com/spf13/afero v1.2.2 // indirect
github.com/spf13/cobra v0.0.3 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/viper v1.3.2 // indirect
github.com/stretchr/testify v1.3.0 // indirect github.com/stretchr/testify v1.3.0 // indirect
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect
github.com/valyala/fasttemplate v1.0.1 // indirect github.com/valyala/fasttemplate v1.0.1 // indirect
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c // indirect
golang.org/x/net v0.0.0-20190328230028-74de082e2cca golang.org/x/net v0.0.0-20190328230028-74de082e2cca
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 golang.org/x/sync v0.0.0-20190423024810-112230192c58
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
sourcegraph.com/sqs/pbtypes v1.0.0 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect
) )

210
go.sum
View File

@ -1,249 +1,105 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/OpenPeeDeeP/depguard v0.0.0-20181229194401-1f388ab2d810/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/antchfx/htmlquery v1.0.0 h1:O5IXz8fZF3B3MW+B33MZWbTHBlYmcfw0BAxgErHuaMA=
github.com/antchfx/htmlquery v1.0.0/go.mod h1:MS9yksVSQXls00iXkiMqXr0J+umL/AmxXKuP28SUJM8=
github.com/antchfx/xpath v1.0.0 h1:Q5gFgh2O40VTSwMOVbFE7nFNRBu3tS21Tn0KAWeEjtk=
github.com/antchfx/xpath v1.0.0/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
github.com/antlr/antlr4 v0.0.0-20190325153624-837aa60e2c47 h1:Lp5nUoQzppfVmfZadpzAytNyb5IMtxyOJLzoQS5dExg= github.com/antlr/antlr4 v0.0.0-20190325153624-837aa60e2c47 h1:Lp5nUoQzppfVmfZadpzAytNyb5IMtxyOJLzoQS5dExg=
github.com/antlr/antlr4 v0.0.0-20190325153624-837aa60e2c47/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y= github.com/antlr/antlr4 v0.0.0-20190325153624-837aa60e2c47/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/coreos/etcd v3.3.12+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/corpix/uarand v0.0.0 h1:mNbzro1GwUcZ1hmO2rWXytkR3JBxNxxctzjyuhO+Aig= github.com/corpix/uarand v0.0.0 h1:mNbzro1GwUcZ1hmO2rWXytkR3JBxNxxctzjyuhO+Aig=
github.com/corpix/uarand v0.0.0/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M= github.com/corpix/uarand v0.0.0/go.mod h1:JSm890tOkDN+M1jqN8pUGDKnzJrsVbJwSMHBY4zwz7M=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9 h1:aSaTVlEXc2QKl4fzXU1tMYCjlrSc2mA4DZtiVfckQHo= github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9 h1:aSaTVlEXc2QKl4fzXU1tMYCjlrSc2mA4DZtiVfckQHo=
github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9/go.mod h1:D6ICZm05D9VN1n/8iOtBxLpXtoGp6HDFUJ1RNVieOSE= github.com/derekparker/trie v0.0.0-20190322172448-1ce4922c7ad9/go.mod h1:D6ICZm05D9VN1n/8iOtBxLpXtoGp6HDFUJ1RNVieOSE=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-critic/go-critic v0.0.0-20181204210945-ee9bf5809ead/go.mod h1:3MzXZKJdeXqdU9cj+rvZdNiN7SZ8V9OjybF8loZDmHU=
github.com/go-critic/go-critic v0.3.4/go.mod h1:AHR42Lk/E/aOznsrYdMYeIQS5RH10HZHSqP+rD6AJrc=
github.com/go-lintpack/lintpack v0.5.1/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-toolsmith/astcast v0.0.0-20181028201508-b7a89ed70af1/go.mod h1:TEo3Ghaj7PsZawQHxT/oBvo4HK/sl1RcuUHDKTTju+o=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v0.0.0-20180903214859-79b422d080c4/go.mod h1:c9CPdq2AzM8oPomdlPniEfPAC6g1s7NqZzODt8y6ib8=
github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ=
github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY=
github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg=
github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw=
github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk=
github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI=
github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks=
github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc=
github.com/go-toolsmith/strparse v0.0.0-20180903215201-830b6daa1241/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/typep v0.0.0-20181030061450-d63dc7650676/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk=
github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0=
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8=
github.com/golangci/go-tools v0.0.0-20180109140146-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/go-tools v0.0.0-20190124090046-35a9f45a5db0/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM=
github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o=
github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU=
github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU=
github.com/golangci/golangci-lint v1.15.0/go.mod h1:iEsyA2h6yMxPzFAlb/Q9UuXBrXIDtXkbUoukuqUAX/8=
github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU=
github.com/golangci/gosec v0.0.0-20180901114220-8afd9cbb6cfb/go.mod h1:ON/c2UR0VAAv6ZEAFKhjCLplESSmRFfZcDLASbI1GWo=
github.com/golangci/govet v0.0.0-20180818181408-44ddbe260190/go.mod h1:pPwb+AK755h3/r73avHz5bEN6sa51/2HEZlLaV53hCo=
github.com/golangci/ineffassign v0.0.0-20180808204949-2ee8f2867dde/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU=
github.com/golangci/lint-1 v0.0.0-20180610141402-4bf9709227d1/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o=
github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA=
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI=
github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f h1:4Gslotqbs16iAg+1KR/XdabIfq8TlAWHdwS5QJFksLc=
github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190328170749-bb2674552d8f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/mafredri/cdp v0.23.4 h1:ffp4qq6slfCL4rFWBDeRHapkLE776gER4tX5Z3LS8CY=
github.com/mafredri/cdp v0.23.1 h1:aqW20I/3CzR8/8VEj+d4zV97l3GU7VdCgi8OTGeJKkA= github.com/mafredri/cdp v0.23.4/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
github.com/mafredri/cdp v0.23.1/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/mozilla/tls-observatory v0.0.0-20190313211306-43961c0c7a1f/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM= github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk= github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0 h1:hSNcYHyxDWycfePW7pUI8swuFkcSMPKh3E63Pokg1Hk=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.14.0 h1:F2F6pGdMrQHGPwr05uwcQNSiWnX5PD76SWw/mYvRBXs=
github.com/rs/zerolog v1.14.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0= github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg= github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k= github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0 h1:X9XMOYjxEfAYSy3xK1DzO5dMkkWhs9E9UCcS1IERx2k=
github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns= github.com/sethgrid/pester v0.0.0-20190127155807-68a33a018ad0/go.mod h1:Ad7IjTpvzZO8Fl0vh9AzQ+j/jYZfyp2diGwI8m5q+ns=
github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil v2.18.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go v0.0.0-20190121191506-3fef8c783dec/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac h1:wbW+Bybf9pXxnCFAOWZTqkRjAc7rAIwo2e1ArUhiHxg= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac h1:wbW+Bybf9pXxnCFAOWZTqkRjAc7rAIwo2e1ArUhiHxg=
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff h1:86HlEv0yBCry9syNuylzqznKXDK11p6D0DT596yNMys= github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff h1:86HlEv0yBCry9syNuylzqznKXDK11p6D0DT596yNMys=
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI= golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181205014116-22934f0fdb62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-379209517ffe/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190213192042-740235f6c0d8/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc=
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY=
mvdan.cc/unparam v0.0.0-20190310220240-1b9ccfa71afe/go.mod h1:BnhuWBAqxH3+J5bDybdxgw5ZfS+DsVd4iylsKQePN8o=
sourcegraph.com/sourcegraph/go-diff v0.5.1-0.20190210232911-dee78e514455/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
sourcegraph.com/sqs/pbtypes v1.0.0/go.mod h1:3AciMUv4qUuRHRHhOG4TZOB+72GdPVz5k+c648qsFS4=

View File

@ -1,6 +1,7 @@
package compiler package compiler
import ( import (
"regexp"
"strings" "strings"
"github.com/MontFerret/ferret/pkg/parser" "github.com/MontFerret/ferret/pkg/parser"
@ -10,6 +11,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var fnNameValidation = regexp.MustCompile("^[a-zA-Z]+[a-zA-Z0-9_]*(::[a-zA-Z]+[a-zA-Z0-9_]*)*$")
type FqlCompiler struct { type FqlCompiler struct {
funcs map[string]core.Function funcs map[string]core.Function
} }
@ -38,6 +41,11 @@ func (c *FqlCompiler) RegisterFunction(name string, fun core.Function) error {
return errors.Errorf("function already exists: %s", name) return errors.Errorf("function already exists: %s", name)
} }
// validation the name
if !fnNameValidation.MatchString(name) {
return errors.Errorf("invalid function name: %s", name)
}
c.funcs[strings.ToUpper(name)] = fun c.funcs[strings.ToUpper(name)] = fun
return nil return nil
@ -57,6 +65,28 @@ func (c *FqlCompiler) RegisterFunctions(funcs map[string]core.Function) error {
return nil return nil
} }
func (c *FqlCompiler) RegisteredFunctions() []string {
res := make([]string, 0, len(c.funcs))
for k := range c.funcs {
res = append(res, k)
}
return res
}
func (c *FqlCompiler) RegisteredFunctionsNS(namespace string) []string {
res := make([]string, 0, len(c.funcs))
for k := range c.funcs {
if strings.HasPrefix(k, namespace) {
res = append(res, k)
}
}
return res
}
func (c *FqlCompiler) Compile(query string) (program *runtime.Program, err error) { func (c *FqlCompiler) Compile(query string) (program *runtime.Program, err error) {
if query == "" { if query == "" {
return nil, ErrEmptyQuery return nil, ErrEmptyQuery
@ -103,10 +133,3 @@ func (c *FqlCompiler) MustCompile(query string) *runtime.Program {
return program return program
} }
func (c *FqlCompiler) RegisteredFunctions() (funcs []string) {
for k := range c.funcs {
funcs = append(funcs, k)
}
return
}

View File

@ -0,0 +1,94 @@
package compiler_test
import (
"context"
"github.com/MontFerret/ferret/pkg/compiler"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
func TestFunctionNSCall(t *testing.T) {
Convey("Should compile RETURN T::SPY", t, func() {
c := compiler.New()
var counter int
err := c.RegisterFunction("T::SPY", func(_ context.Context, _ ...core.Value) (core.Value, error) {
counter++
return values.None, nil
})
So(err, ShouldBeNil)
p, err := c.Compile(`
RETURN T::SPY()
`)
So(err, ShouldBeNil)
_, err = p.Run(context.Background())
So(err, ShouldBeNil)
So(counter, ShouldEqual, 1)
})
Convey("Should compile RETURN T::UTILS::SPY", t, func() {
c := compiler.New()
var counter int
err := c.RegisterFunction("T::UTILS::SPY", func(_ context.Context, _ ...core.Value) (core.Value, error) {
counter++
return values.None, nil
})
So(err, ShouldBeNil)
p, err := c.Compile(`
RETURN T::UTILS::SPY()
`)
So(err, ShouldBeNil)
_, err = p.Run(context.Background())
So(err, ShouldBeNil)
So(counter, ShouldEqual, 1)
})
Convey("Should NOT compile RETURN T:UTILS::SPY", t, func() {
c := compiler.New()
var counter int
err := c.RegisterFunction("T::UTILS::SPY", func(_ context.Context, _ ...core.Value) (core.Value, error) {
counter++
return values.None, nil
})
So(err, ShouldBeNil)
_, err = c.Compile(`
RETURN T:UTILS::SPY()
`)
So(err, ShouldNotBeNil)
})
Convey("Should NOT register RETURN T:UTILS::SPY", t, func() {
c := compiler.New()
var counter int
err := c.RegisterFunction("T::UTILS:SPY", func(_ context.Context, _ ...core.Value) (core.Value, error) {
counter++
return values.None, nil
})
So(err, ShouldNotBeNil)
})
}

View File

@ -24,13 +24,13 @@ BAR
}) })
Convey("Should be possible to use multi line string with nested strings", t, func() { Convey("Should be possible to use multi line string with nested strings", t, func() {
out := compiler.New(). compiler.New().
MustCompile(fmt.Sprintf(` MustCompile(fmt.Sprintf(`
RETURN %s<!DOCTYPE html> RETURN %s<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Title</title> <title>GetTitle</title>
</head> </head>
<body> <body>
Hello world Hello world
@ -43,7 +43,7 @@ RETURN %s<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Title</title> <title>GetTitle</title>
</head> </head>
<body> <body>
Hello world Hello world

View File

@ -1,11 +0,0 @@
package compiler_test
import (
"context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
func NOOP(_ context.Context, args ...core.Value) (core.Value, error) {
return values.None, nil
}

View File

@ -426,7 +426,7 @@ func (v *visitor) doVisitCollectClause(ctx *fql.CollectClauseContext, scope *sco
collectSelectors := groupingCtx.AllCollectSelector() collectSelectors := groupingCtx.AllCollectSelector()
// group selectors // group selectors
if collectSelectors != nil && len(collectSelectors) > 0 { if len(collectSelectors) > 0 {
selectors = make([]*clauses.CollectSelector, 0, len(collectSelectors)) selectors = make([]*clauses.CollectSelector, 0, len(collectSelectors))
for _, cs := range collectSelectors { for _, cs := range collectSelectors {
@ -481,10 +481,6 @@ func (v *visitor) doVisitCollectClause(ctx *fql.CollectClauseContext, scope *sco
projectionSelectorExp := literals.NewObjectLiteralWith(propExp) projectionSelectorExp := literals.NewObjectLiteralWith(propExp)
if err != nil {
return nil, err
}
selector, err := clauses.NewCollectSelector(projectionIdentifier.GetText(), projectionSelectorExp) selector, err := clauses.NewCollectSelector(projectionIdentifier.GetText(), projectionSelectorExp)
if err != nil { if err != nil {
@ -1049,12 +1045,20 @@ func (v *visitor) doVisitFunctionCallExpression(context *fql.FunctionCallExpress
} }
} }
funcName := context.Identifier().GetText() var name string
fun, exists := v.funcs[funcName] funcNS := context.Namespace()
if funcNS != nil {
name += funcNS.GetText()
}
name += context.Identifier().GetText()
fun, exists := v.funcs[name]
if !exists { if !exists {
return nil, core.Error(core.ErrNotFound, fmt.Sprintf("function: '%s'", funcName)) return nil, core.Error(core.ErrNotFound, fmt.Sprintf("function: '%s'", name))
} }
return expressions.NewFunctionCallExpression( return expressions.NewFunctionCallExpression(
@ -1413,7 +1417,7 @@ func (v *visitor) doVisitChildren(node antlr.RuleNode, scope *scope) ([]core.Exp
children := node.GetChildren() children := node.GetChildren()
if children == nil { if children == nil {
return make([]core.Expression, 0, 0), nil return make([]core.Expression, 0), nil
} }
result := make([]core.Expression, 0, len(children)) result := make([]core.Expression, 0, len(children))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
package cdp

View File

@ -4,24 +4,21 @@ import (
"context" "context"
"sync" "sync"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/common"
"github.com/MontFerret/ferret/pkg/runtime/logging"
"github.com/mafredri/cdp" "github.com/mafredri/cdp"
"github.com/mafredri/cdp/devtool" "github.com/mafredri/cdp/devtool"
"github.com/mafredri/cdp/protocol/emulation"
"github.com/mafredri/cdp/protocol/network"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/protocol/target" "github.com/mafredri/cdp/protocol/target"
"github.com/mafredri/cdp/rpcc" "github.com/mafredri/cdp/rpcc"
"github.com/mafredri/cdp/session" "github.com/mafredri/cdp/session"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/logging"
) )
const DriverName = "cdp" const DriverName = "cdp"
type Driver struct { type Driver struct {
sync.Mutex mu sync.Mutex
dev *devtool.DevTools dev *devtool.DevTools
conn *rpcc.Conn conn *rpcc.Conn
client *cdp.Client client *cdp.Client
@ -42,7 +39,7 @@ func (drv *Driver) Name() string {
return drv.options.Name return drv.options.Name
} }
func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocumentParams) (drivers.HTMLDocument, error) { func (drv *Driver) Open(ctx context.Context, params drivers.OpenPageParams) (drivers.HTMLPage, error) {
logger := logging.FromContext(ctx) logger := logging.FromContext(ctx)
err := drv.init(ctx) err := drv.init(ctx)
@ -58,20 +55,15 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument
return nil, err return nil, err
} }
url := params.URL // Args for a new target belonging to the browser context
createTargetArgs := target.NewCreateTargetArgs(BlankPageURL)
if url == "" { if !drv.options.KeepCookies && !params.KeepCookies {
url = BlankPageURL
}
// Create a new target belonging to the browser context
createTargetArgs := target.NewCreateTargetArgs(url)
if drv.options.KeepCookies == false && params.KeepCookies == false {
// Set it to an incognito mode // Set it to an incognito mode
createTargetArgs.SetBrowserContextID(drv.contextID) createTargetArgs.SetBrowserContextID(drv.contextID)
} }
// New target
createTarget, err := drv.client.Target.CreateTarget(ctx, createTargetArgs) createTarget, err := drv.client.Target.CreateTarget(ctx, createTargetArgs)
if err != nil { if err != nil {
@ -99,69 +91,16 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument
return nil, err return nil, err
} }
client := cdp.NewClient(conn) if params.UserAgent == "" {
params.UserAgent = drv.options.UserAgent
err = runBatch(
func() error {
return client.Page.Enable(ctx)
},
func() error {
return client.Page.SetLifecycleEventsEnabled(
ctx,
page.NewSetLifecycleEventsEnabledArgs(true),
)
},
func() error {
return client.DOM.Enable(ctx)
},
func() error {
return client.Runtime.Enable(ctx)
},
func() error {
var ua string
if params.UserAgent != "" {
ua = common.GetUserAgent(params.UserAgent)
} else {
ua = common.GetUserAgent(drv.options.UserAgent)
} }
logger. return LoadHTMLPage(ctx, conn, params)
Debug().
Timestamp().
Str("user-agent", ua).
Msg("using User-Agent")
// do not use custom user agent
if ua == "" {
return nil
}
return client.Emulation.SetUserAgentOverride(
ctx,
emulation.NewSetUserAgentOverrideArgs(ua),
)
},
func() error {
return client.Network.Enable(ctx, network.NewEnableArgs())
},
)
if err != nil {
return nil, err
}
return LoadHTMLDocument(ctx, conn, client, params)
} }
func (drv *Driver) Close() error { func (drv *Driver) Close() error {
drv.Lock() drv.mu.Lock()
defer drv.Unlock() defer drv.mu.Unlock()
if drv.session != nil { if drv.session != nil {
drv.session.Close() drv.session.Close()
@ -173,8 +112,8 @@ func (drv *Driver) Close() error {
} }
func (drv *Driver) init(ctx context.Context) error { func (drv *Driver) init(ctx context.Context) error {
drv.Lock() drv.mu.Lock()
defer drv.Unlock() defer drv.mu.Unlock()
if drv.session == nil { if drv.session == nil {
ver, err := drv.dev.Version(ctx) ver, err := drv.dev.Version(ctx)

View File

@ -4,34 +4,34 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
"github.com/pkg/errors"
"golang.org/x/net/html"
"hash/fnv" "hash/fnv"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"github.com/MontFerret/ferret/pkg/drivers/cdp/events" "github.com/MontFerret/ferret/pkg/drivers/cdp/events"
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
"github.com/MontFerret/ferret/pkg/drivers/common" "github.com/MontFerret/ferret/pkg/drivers/common"
"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/MontFerret/ferret/pkg/runtime/values/types" "github.com/MontFerret/ferret/pkg/runtime/values/types"
"github.com/gofrs/uuid"
"github.com/mafredri/cdp" "github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/input"
"github.com/mafredri/cdp/protocol/runtime" "github.com/mafredri/cdp/protocol/runtime"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
var emptyNodeID = dom.NodeID(0) var emptyNodeID = dom.NodeID(0)
var emptyBackendID = dom.BackendNodeID(0)
type ( type (
HTMLElementIdentity struct { HTMLElementIdentity struct {
nodeID dom.NodeID nodeID dom.NodeID
backendID dom.BackendNodeID
objectID runtime.RemoteObjectID objectID runtime.RemoteObjectID
} }
@ -40,40 +40,37 @@ type (
logger *zerolog.Logger logger *zerolog.Logger
client *cdp.Client client *cdp.Client
events *events.EventBroker events *events.EventBroker
input *input.Manager
exec *eval.ExecutionContext
connected values.Boolean connected values.Boolean
id *HTMLElementIdentity id HTMLElementIdentity
nodeType values.Int nodeType html.NodeType
nodeName values.String nodeName values.String
innerHTML values.String innerHTML values.String
innerText *common.LazyValue innerText *common.LazyValue
value core.Value value core.Value
attributes *common.LazyValue attributes *common.LazyValue
style *common.LazyValue style *common.LazyValue
children []*HTMLElementIdentity children []HTMLElementIdentity
loadedChildren *common.LazyValue loadedChildren *common.LazyValue
} }
) )
func LoadElement( func LoadHTMLElement(
ctx context.Context, ctx context.Context,
logger *zerolog.Logger, logger *zerolog.Logger,
client *cdp.Client, client *cdp.Client,
broker *events.EventBroker, broker *events.EventBroker,
input *input.Manager,
exec *eval.ExecutionContext,
nodeID dom.NodeID, nodeID dom.NodeID,
backendID dom.BackendNodeID,
) (*HTMLElement, error) { ) (*HTMLElement, error) {
if client == nil { if client == nil {
return nil, core.Error(core.ErrMissedArgument, "client") return nil, core.Error(core.ErrMissedArgument, "client")
} }
// getting a remote object that represents the current DOM Node // getting a remote object that represents the current DOM Node
var args *dom.ResolveNodeArgs args := dom.NewResolveNodeArgs().SetNodeID(nodeID).SetExecutionContextID(exec.ID())
if backendID > 0 {
args = dom.NewResolveNodeArgs().SetBackendNodeID(backendID)
} else {
args = dom.NewResolveNodeArgs().SetNodeID(nodeID)
}
obj, err := client.DOM.ResolveNode(ctx, args) obj, err := client.DOM.ResolveNode(ctx, args)
@ -85,34 +82,46 @@ func LoadElement(
return nil, core.Error(core.ErrNotFound, fmt.Sprintf("element %d", nodeID)) return nil, core.Error(core.ErrNotFound, fmt.Sprintf("element %d", nodeID))
} }
objectID := *obj.Object.ObjectID id := HTMLElementIdentity{}
id.nodeID = nodeID
id.objectID = *obj.Object.ObjectID
return LoadHTMLElementWithID(
ctx,
logger,
client,
broker,
input,
exec,
id,
)
}
func LoadHTMLElementWithID(
ctx context.Context,
logger *zerolog.Logger,
client *cdp.Client,
broker *events.EventBroker,
input *input.Manager,
exec *eval.ExecutionContext,
id HTMLElementIdentity,
) (*HTMLElement, error) {
node, err := client.DOM.DescribeNode( node, err := client.DOM.DescribeNode(
ctx, ctx,
dom. dom.
NewDescribeNodeArgs(). NewDescribeNodeArgs().
SetObjectID(objectID). SetObjectID(id.objectID).
SetDepth(1), SetDepth(1),
) )
if err != nil { if err != nil {
return nil, core.Error(err, strconv.Itoa(int(nodeID))) return nil, core.Error(err, strconv.Itoa(int(id.nodeID)))
} }
id := new(HTMLElementIdentity) innerHTML, err := loadInnerHTML(ctx, client, exec, id, common.ToHTMLType(node.Node.NodeType))
id.nodeID = nodeID
id.objectID = objectID
if backendID > 0 {
id.backendID = backendID
} else {
id.backendID = node.Node.BackendNodeID
}
innerHTML, err := loadInnerHTML(ctx, client, id)
if err != nil { if err != nil {
return nil, core.Error(err, strconv.Itoa(int(nodeID))) return nil, core.Error(err, strconv.Itoa(int(id.nodeID)))
} }
var val string var val string
@ -125,6 +134,8 @@ func LoadElement(
logger, logger,
client, client,
broker, broker,
input,
exec,
id, id,
node.Node.NodeType, node.Node.NodeType,
node.Node.NodeName, node.Node.NodeName,
@ -138,20 +149,24 @@ func NewHTMLElement(
logger *zerolog.Logger, logger *zerolog.Logger,
client *cdp.Client, client *cdp.Client,
broker *events.EventBroker, broker *events.EventBroker,
id *HTMLElementIdentity, input *input.Manager,
exec *eval.ExecutionContext,
id HTMLElementIdentity,
nodeType int, nodeType int,
nodeName string, nodeName string,
value string, value string,
innerHTML values.String, innerHTML values.String,
children []*HTMLElementIdentity, children []HTMLElementIdentity,
) *HTMLElement { ) *HTMLElement {
el := new(HTMLElement) el := new(HTMLElement)
el.logger = logger el.logger = logger
el.client = client el.client = client
el.events = broker el.events = broker
el.input = input
el.exec = exec
el.connected = values.True el.connected = values.True
el.id = id el.id = id
el.nodeType = values.NewInt(nodeType) el.nodeType = common.ToHTMLType(nodeType)
el.nodeName = values.NewString(nodeName) el.nodeName = values.NewString(nodeName)
el.innerHTML = innerHTML el.innerHTML = innerHTML
el.innerText = common.NewLazyValue(el.loadInnerText) el.innerText = common.NewLazyValue(el.loadInnerText)
@ -207,7 +222,7 @@ func (el *HTMLElement) MarshalJSON() ([]byte, error) {
} }
func (el *HTMLElement) String() string { func (el *HTMLElement) String() string {
return el.InnerHTML(context.Background()).String() return el.GetInnerHTML(context.Background()).String()
} }
func (el *HTMLElement) Compare(other core.Value) int64 { func (el *HTMLElement) Compare(other core.Value) int64 {
@ -217,7 +232,7 @@ func (el *HTMLElement) Compare(other core.Value) int64 {
ctx := context.Background() ctx := context.Background()
return el.InnerHTML(ctx).Compare(other.InnerHTML(ctx)) return el.GetInnerHTML(ctx).Compare(other.GetInnerHTML(ctx))
default: default:
return drivers.Compare(el.Type(), other.Type()) return drivers.Compare(el.Type(), other.Type())
} }
@ -257,11 +272,11 @@ func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core.
} }
func (el *HTMLElement) GetValue(ctx context.Context) core.Value { func (el *HTMLElement) GetValue(ctx context.Context) core.Value {
if !el.IsConnected() { if el.IsDetached() {
return el.value return el.value
} }
val, err := eval.Property(ctx, el.client, el.id.objectID, "value") val, err := el.exec.ReadProperty(ctx, el.id.objectID, "value")
if err != nil { if err != nil {
el.logError(err).Msg("failed to get node value") el.logError(err).Msg("failed to get node value")
@ -275,7 +290,7 @@ func (el *HTMLElement) GetValue(ctx context.Context) core.Value {
} }
func (el *HTMLElement) SetValue(ctx context.Context, value core.Value) error { func (el *HTMLElement) SetValue(ctx context.Context, value core.Value) error {
if !el.IsConnected() { if el.IsDetached() {
// TODO: Return an error // TODO: Return an error
return nil return nil
} }
@ -283,11 +298,11 @@ func (el *HTMLElement) SetValue(ctx context.Context, value core.Value) error {
return el.client.DOM.SetNodeValue(ctx, dom.NewSetNodeValueArgs(el.id.nodeID, value.String())) return el.client.DOM.SetNodeValue(ctx, dom.NewSetNodeValueArgs(el.id.nodeID, value.String()))
} }
func (el *HTMLElement) NodeType() values.Int { func (el *HTMLElement) GetNodeType() values.Int {
return el.nodeType return values.NewInt(common.FromHTMLType(el.nodeType))
} }
func (el *HTMLElement) NodeName() values.String { func (el *HTMLElement) GetNodeName() values.String {
return el.nodeName return el.nodeName
} }
@ -472,7 +487,7 @@ func (el *HTMLElement) GetChildNode(ctx context.Context, idx values.Int) core.Va
} }
func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String) core.Value { func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String) core.Value {
if !el.IsConnected() { if el.IsDetached() {
return values.None return values.None
} }
@ -496,7 +511,7 @@ func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String
return values.None return values.None
} }
res, err := LoadElement(ctx, el.logger, el.client, el.events, found.NodeID, emptyBackendID) res, err := LoadHTMLElement(ctx, el.logger, el.client, el.events, el.input, el.exec, found.NodeID)
if err != nil { if err != nil {
el.logError(err). el.logError(err).
@ -510,7 +525,7 @@ func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String
} }
func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.String) core.Value { func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.String) core.Value {
if !el.IsConnected() { if el.IsDetached() {
return values.NewArray(0) return values.NewArray(0)
} }
@ -537,7 +552,7 @@ func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.Str
continue continue
} }
childEl, err := LoadElement(ctx, el.logger, el.client, el.events, id, emptyBackendID) childEl, err := LoadHTMLElement(ctx, el.logger, el.client, el.events, el.input, el.exec, id)
if err != nil { if err != nil {
el.logError(err). el.logError(err).
@ -562,7 +577,154 @@ func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.Str
return arr return arr
} }
func (el *HTMLElement) InnerText(ctx context.Context) values.String { func (el *HTMLElement) XPath(ctx context.Context, expression values.String) (result core.Value, err error) {
exp, err := expression.MarshalJSON()
if err != nil {
return values.None, err
}
out, err := el.exec.CallFunction(ctx, templates.XPath(),
runtime.CallArgument{
ObjectID: &el.id.objectID,
},
runtime.CallArgument{
Value: json.RawMessage(exp),
},
)
if err != nil {
return values.None, err
}
typeName := out.Type
// checking whether it's actually an array
if typeName == "object" {
isArrayRes, err := el.exec.CallFunction(ctx, `
(target) => Array.isArray(target)
`,
runtime.CallArgument{
ObjectID: out.ObjectID,
},
)
if err != nil {
return values.None, err
}
isArray, err := eval.Unmarshal(&isArrayRes)
if err != nil {
return values.None, err
}
if isArray == values.True {
typeName = "array"
}
}
switch typeName {
case "string", "number", "boolean":
return eval.Unmarshal(&out)
case "array":
if out.ObjectID == nil {
return values.None, nil
}
props, err := el.client.Runtime.GetProperties(ctx, runtime.NewGetPropertiesArgs(*out.ObjectID).SetOwnProperties(true))
if err != nil {
return values.None, err
}
if props.ExceptionDetails != nil {
exception := *props.ExceptionDetails
return values.None, errors.New(exception.Text)
}
result := values.NewArray(len(props.Result))
defer func() {
if err != nil {
result.ForEach(func(value core.Value, idx int) bool {
el, ok := value.(*HTMLElement)
if ok {
el.Close()
}
return true
})
}
}()
for _, descr := range props.Result {
if !descr.Enumerable {
continue
}
if descr.Value == nil {
continue
}
repl, err := el.client.DOM.RequestNode(ctx, dom.NewRequestNodeArgs(*descr.Value.ObjectID))
if err != nil {
return values.None, err
}
el, err := LoadHTMLElementWithID(
ctx,
el.logger,
el.client,
el.events,
el.input,
el.exec,
HTMLElementIdentity{
nodeID: repl.NodeID,
objectID: *descr.Value.ObjectID,
},
)
if err != nil {
return values.None, err
}
result.Push(el)
}
return result, nil
case "object":
if out.ObjectID == nil {
return values.None, nil
}
repl, err := el.client.DOM.RequestNode(ctx, dom.NewRequestNodeArgs(*out.ObjectID))
if err != nil {
return values.None, err
}
return LoadHTMLElementWithID(
ctx,
el.logger,
el.client,
el.events,
el.input,
el.exec,
HTMLElementIdentity{
nodeID: repl.NodeID,
objectID: *out.ObjectID,
},
)
default:
return values.None, nil
}
}
func (el *HTMLElement) GetInnerText(ctx context.Context) values.String {
val, err := el.innerText.Read(ctx) val, err := el.innerText.Read(ctx)
if err != nil { if err != nil {
@ -577,7 +739,7 @@ func (el *HTMLElement) InnerText(ctx context.Context) values.String {
} }
func (el *HTMLElement) InnerTextBySelector(ctx context.Context, selector values.String) values.String { func (el *HTMLElement) InnerTextBySelector(ctx context.Context, selector values.String) values.String {
if !el.IsConnected() { if el.IsDetached() {
return values.EmptyString return values.EmptyString
} }
@ -624,7 +786,7 @@ func (el *HTMLElement) InnerTextBySelector(ctx context.Context, selector values.
objID := *obj.Object.ObjectID objID := *obj.Object.ObjectID
text, err := eval.Property(ctx, el.client, objID, "innerText") text, err := el.exec.ReadProperty(ctx, objID, "innerText")
if err != nil { if err != nil {
el.logError(err). el.logError(err).
@ -679,7 +841,7 @@ func (el *HTMLElement) InnerTextBySelectorAll(ctx context.Context, selector valu
objID := *obj.Object.ObjectID objID := *obj.Object.ObjectID
text, err := eval.Property(ctx, el.client, objID, "innerText") text, err := el.exec.ReadProperty(ctx, objID, "innerText")
if err != nil { if err != nil {
el.logError(err). el.logError(err).
@ -696,7 +858,7 @@ func (el *HTMLElement) InnerTextBySelectorAll(ctx context.Context, selector valu
return arr return arr
} }
func (el *HTMLElement) InnerHTML(_ context.Context) values.String { func (el *HTMLElement) GetInnerHTML(_ context.Context) values.String {
el.mu.Lock() el.mu.Lock()
defer el.mu.Unlock() defer el.mu.Unlock()
@ -704,7 +866,7 @@ func (el *HTMLElement) InnerHTML(_ context.Context) values.String {
} }
func (el *HTMLElement) InnerHTMLBySelector(ctx context.Context, selector values.String) values.String { func (el *HTMLElement) InnerHTMLBySelector(ctx context.Context, selector values.String) values.String {
if !el.IsConnected() { if el.IsDetached() {
return values.EmptyString return values.EmptyString
} }
@ -719,13 +881,12 @@ func (el *HTMLElement) InnerHTMLBySelector(ctx context.Context, selector values.
return values.EmptyString return values.EmptyString
} }
text, err := loadInnerHTML(ctx, el.client, &HTMLElementIdentity{ text, err := loadInnerHTMLByNodeID(ctx, el.client, el.exec, found.NodeID)
nodeID: found.NodeID,
})
if err != nil { if err != nil {
el.logError(err). el.logError(err).
Str("selector", selector.String()). Str("selector", selector.String()).
Int("childNodeID", int(found.NodeID)).
Msg("failed to load inner HTML for found child el") Msg("failed to load inner HTML for found child el")
return values.EmptyString return values.EmptyString
@ -750,13 +911,12 @@ func (el *HTMLElement) InnerHTMLBySelectorAll(ctx context.Context, selector valu
arr := values.NewArray(len(res.NodeIDs)) arr := values.NewArray(len(res.NodeIDs))
for _, id := range res.NodeIDs { for _, id := range res.NodeIDs {
text, err := loadInnerHTML(ctx, el.client, &HTMLElementIdentity{ text, err := loadInnerHTMLByNodeID(ctx, el.client, el.exec, id)
nodeID: id,
})
if err != nil { if err != nil {
el.logError(err). el.logError(err).
Str("selector", selector.String()). Str("selector", selector.String()).
Int("childNodeID", int(id)).
Msg("failed to load inner HTML for found child el") Msg("failed to load inner HTML for found child el")
// return what we have // return what we have
@ -770,7 +930,7 @@ func (el *HTMLElement) InnerHTMLBySelectorAll(ctx context.Context, selector valu
} }
func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.String) values.Int { func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.String) values.Int {
if !el.IsConnected() { if el.IsDetached() {
return values.ZeroInt return values.ZeroInt
} }
@ -790,7 +950,7 @@ func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.Stri
} }
func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector values.String) values.Boolean { func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector values.String) values.Boolean {
if !el.IsConnected() { if el.IsDetached() {
return values.False return values.False
} }
@ -887,183 +1047,43 @@ func (el *HTMLElement) WaitForStyle(ctx context.Context, name values.String, val
} }
func (el *HTMLElement) Click(ctx context.Context) (values.Boolean, error) { func (el *HTMLElement) Click(ctx context.Context) (values.Boolean, error) {
return events.DispatchEvent(ctx, el.client, el.id.objectID, "click") if err := el.input.ClickByNodeID(ctx, el.id.nodeID); err != nil {
return values.False, err
}
return values.True, nil
} }
func (el *HTMLElement) Input(ctx context.Context, value core.Value, delay values.Int) error { func (el *HTMLElement) Input(ctx context.Context, value core.Value, delay values.Int) error {
if err := el.client.DOM.Focus(ctx, dom.NewFocusArgs().SetObjectID(el.id.objectID)); err != nil { if el.GetNodeName() != "INPUT" {
el.logError(err).Msg("failed to focus") return core.Error(core.ErrInvalidOperation, "element is not an <input> element.")
return err
} }
delayMs := time.Duration(delay) return el.input.TypeByNodeID(ctx, el.id.nodeID, value, delay)
time.Sleep(delayMs * time.Millisecond)
valStr := value.String()
for _, ch := range valStr {
for _, ev := range []string{"keyDown", "keyUp"} {
ke := input.NewDispatchKeyEventArgs(ev).SetText(string(ch))
if err := el.client.Input.DispatchKeyEvent(ctx, ke); err != nil {
el.logError(err).Str("value", value.String()).Msg("failed to input a value")
return err
}
time.Sleep(delayMs * time.Millisecond)
}
}
return nil
} }
func (el *HTMLElement) Select(ctx context.Context, value *values.Array) (*values.Array, error) { func (el *HTMLElement) Select(ctx context.Context, value *values.Array) (*values.Array, error) {
var attrID = "data-ferret-select" return el.input.SelectByNodeID(ctx, el.id.nodeID, value)
if el.NodeName() != "SELECT" {
return nil, core.Error(core.ErrInvalidOperation, "element is not a <select> element.")
}
id, err := uuid.NewV4()
if err != nil {
return nil, err
}
err = el.client.DOM.SetAttributeValue(ctx, dom.NewSetAttributeValueArgs(el.id.nodeID, attrID, id.String()))
if err != nil {
return nil, err
}
res, err := eval.Eval(
ctx,
el.client,
fmt.Sprintf(`
var el = document.querySelector('[%s="%s"]');
if (el == null) {
return [];
}
var values = %s;
if (el.nodeName.toLowerCase() !== 'select') {
throw new Error('element is not a <select> element.');
}
var options = Array.from(el.options);
el.value = undefined;
for (var option of options) {
option.selected = values.includes(option.value);
if (option.selected && !el.multiple) {
break;
}
}
el.dispatchEvent(new Event('input', { 'bubbles': true }));
el.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
`,
attrID,
id.String(),
value.String(),
),
true,
false,
)
if err != nil {
return nil, err
}
err = el.client.DOM.RemoveAttribute(ctx, dom.NewRemoveAttributeArgs(el.id.nodeID, attrID))
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 (el *HTMLElement) ScrollIntoView(ctx context.Context) error { func (el *HTMLElement) ScrollIntoView(ctx context.Context) error {
var attrID = "data-ferret-scroll" return el.input.ScrollIntoViewByNodeID(ctx, el.id.nodeID)
id, err := uuid.NewV4()
if err != nil {
return err
}
err = el.client.DOM.SetAttributeValue(ctx, dom.NewSetAttributeValueArgs(el.id.nodeID, attrID, id.String()))
if err != nil {
return err
}
_, err = eval.Eval(
ctx,
el.client,
fmt.Sprintf(`
var el = document.querySelector('[%s="%s"]');
if (el == null) {
throw new Error('element not found');
}
el.scrollIntoView({
behavior: 'instant',
inline: 'center',
block: 'center'
});
`,
attrID,
id.String(),
), false, false)
if err != nil {
return err
}
err = el.client.DOM.RemoveAttribute(ctx, dom.NewRemoveAttributeArgs(el.id.nodeID, attrID))
return err
} }
func (el *HTMLElement) Hover(ctx context.Context) error { func (el *HTMLElement) Hover(ctx context.Context) error {
err := el.ScrollIntoView(ctx) return el.input.MoveMouseByNodeID(ctx, el.id.nodeID)
if err != nil {
return err
} }
q, err := getClickablePoint(ctx, el.client, el.id) func (el *HTMLElement) IsDetached() values.Boolean {
if err != nil {
return err
}
return el.client.Input.DispatchMouseEvent(
ctx,
input.NewDispatchMouseEventArgs("mouseMoved", q.X, q.Y),
)
}
func (el *HTMLElement) IsConnected() values.Boolean {
el.mu.Lock() el.mu.Lock()
defer el.mu.Unlock() defer el.mu.Unlock()
return el.connected return !el.connected
} }
func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) { func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) {
if el.IsConnected() { if !el.IsDetached() {
text, err := loadInnerText(ctx, el.client, el.id) text, err := loadInnerText(ctx, el.client, el.exec, el.id, el.nodeType)
if err == nil { if err == nil {
return text, nil return text, nil
@ -1074,7 +1094,7 @@ func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) {
// and just parse cached innerHTML // and just parse cached innerHTML
} }
h := el.InnerHTML(ctx) h := el.GetInnerHTML(ctx)
if h == values.EmptyString { if h == values.EmptyString {
return h, nil return h, nil
@ -1102,20 +1122,21 @@ func (el *HTMLElement) loadAttrs(ctx context.Context) (core.Value, error) {
} }
func (el *HTMLElement) loadChildren(ctx context.Context) (core.Value, error) { func (el *HTMLElement) loadChildren(ctx context.Context) (core.Value, error) {
if !el.IsConnected() { if el.IsDetached() {
return values.NewArray(0), nil return values.NewArray(0), nil
} }
loaded := values.NewArray(len(el.children)) loaded := values.NewArray(len(el.children))
for _, childID := range el.children { for _, childID := range el.children {
child, err := LoadElement( child, err := LoadHTMLElement(
ctx, ctx,
el.logger, el.logger,
el.client, el.client,
el.events, el.events,
el.input,
el.exec,
childID.nodeID, childID.nodeID,
childID.backendID,
) )
if err != nil { if err != nil {
@ -1285,21 +1306,20 @@ func (el *HTMLElement) handleChildInserted(ctx context.Context, message interfac
return return
} }
nextIdentity := &HTMLElementIdentity{ nextIdentity := HTMLElementIdentity{
nodeID: reply.Node.NodeID, nodeID: reply.Node.NodeID,
backendID: reply.Node.BackendNodeID,
} }
arr := el.children arr := el.children
el.children = append(arr[:targetIDx], append([]*HTMLElementIdentity{nextIdentity}, arr[targetIDx:]...)...) el.children = append(arr[:targetIDx], append([]HTMLElementIdentity{nextIdentity}, arr[targetIDx:]...)...)
if !el.loadedChildren.Ready() { if !el.loadedChildren.Ready() {
return return
} }
el.loadedChildren.Write(ctx, func(v core.Value, err error) { el.loadedChildren.Write(ctx, func(v core.Value, _ error) {
loadedArr := v.(*values.Array) loadedArr := v.(*values.Array)
loadedEl, err := LoadElement(ctx, el.logger, el.client, el.events, nextID, emptyBackendID) loadedEl, err := LoadHTMLElement(ctx, el.logger, el.client, el.events, el.input, el.exec, nextID)
if err != nil { if err != nil {
el.logError(err).Msg("failed to load an inserted element") el.logError(err).Msg("failed to load an inserted element")
@ -1309,7 +1329,7 @@ func (el *HTMLElement) handleChildInserted(ctx context.Context, message interfac
loadedArr.Insert(values.NewInt(targetIDx), loadedEl) loadedArr.Insert(values.NewInt(targetIDx), loadedEl)
newInnerHTML, err := loadInnerHTML(ctx, el.client, el.id) newInnerHTML, err := loadInnerHTML(ctx, el.client, el.exec, el.id, el.nodeType)
if err != nil { if err != nil {
el.logError(err).Msg("failed to update element") el.logError(err).Msg("failed to update element")
@ -1371,7 +1391,7 @@ func (el *HTMLElement) handleChildRemoved(ctx context.Context, message interface
loadedArr := v.(*values.Array) loadedArr := v.(*values.Array)
loadedArr.RemoveAt(values.NewInt(targetIDx)) loadedArr.RemoveAt(values.NewInt(targetIDx))
newInnerHTML, err := loadInnerHTML(ctx, el.client, el.id) newInnerHTML, err := loadInnerHTML(ctx, el.client, el.exec, el.id, el.nodeType)
if err != nil { if err != nil {
el.logger.Error(). el.logger.Error().
@ -1393,7 +1413,6 @@ func (el *HTMLElement) logError(err error) *zerolog.Event {
Error(). Error().
Timestamp(). Timestamp().
Int("nodeID", int(el.id.nodeID)). Int("nodeID", int(el.id.nodeID)).
Int("backendID", int(el.id.backendID)).
Str("objectID", string(el.id.objectID)). Str("objectID", string(el.id.objectID)).
Err(err) Err(err)
} }

View File

@ -0,0 +1,265 @@
package eval
import (
"context"
"fmt"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/protocol/runtime"
"github.com/pkg/errors"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
const EmptyExecutionContextID = runtime.ExecutionContextID(-1)
type ExecutionContext struct {
client *cdp.Client
frame page.Frame
contextID runtime.ExecutionContextID
}
func NewExecutionContext(client *cdp.Client, frame page.Frame, contextID runtime.ExecutionContextID) *ExecutionContext {
ec := new(ExecutionContext)
ec.client = client
ec.frame = frame
ec.contextID = contextID
return ec
}
func (ec *ExecutionContext) ID() runtime.ExecutionContextID {
return ec.contextID
}
func (ec *ExecutionContext) Eval(ctx context.Context, exp string) error {
_, err := ec.evalWithValueInternal(
ctx,
runtime.
NewEvaluateArgs(PrepareEval(exp)),
)
return err
}
func (ec *ExecutionContext) EvalWithValue(ctx context.Context, exp string) (core.Value, error) {
return ec.evalWithValueInternal(
ctx,
runtime.
NewEvaluateArgs(PrepareEval(exp)).
SetReturnByValue(true),
)
}
func (ec *ExecutionContext) EvalAsync(ctx context.Context, exp string) (core.Value, error) {
return ec.evalWithValueInternal(
ctx,
runtime.
NewEvaluateArgs(PrepareEval(exp)).
SetReturnByValue(true).
SetAwaitPromise(true),
)
}
func (ec *ExecutionContext) ResolveRemoteObject(ctx context.Context, exp string) (runtime.RemoteObject, error) {
res, err := ec.evalInternal(ctx, runtime.NewEvaluateArgs(PrepareEval(exp)))
if err != nil {
return runtime.RemoteObject{}, err
}
if res.ObjectID == nil {
return runtime.RemoteObject{}, errors.Wrap(core.ErrUnexpected, "unable to resolve remote object")
}
return res, nil
}
func (ec *ExecutionContext) CallMethod(
ctx context.Context,
objectID runtime.RemoteObjectID,
methodName string,
args []runtime.CallArgument,
) (*runtime.RemoteObject, error) {
callArgs := runtime.NewCallFunctionOnArgs(methodName).
SetObjectID(objectID).
SetArguments(args)
if ec.contextID != EmptyExecutionContextID {
callArgs.SetExecutionContextID(ec.contextID)
}
found, err := ec.client.Runtime.CallFunctionOn(
ctx,
callArgs,
)
if err != nil {
return nil, err
}
if found.ExceptionDetails != nil {
return nil, found.ExceptionDetails
}
if found.Result.ObjectID == nil {
return nil, nil
}
return &found.Result, nil
}
func (ec *ExecutionContext) ReadProperty(
ctx context.Context,
objectID runtime.RemoteObjectID,
propName string,
) (core.Value, error) {
res, err := ec.client.Runtime.GetProperties(
ctx,
runtime.NewGetPropertiesArgs(objectID),
)
if err != nil {
return values.None, err
}
if res.ExceptionDetails != nil {
return values.None, res.ExceptionDetails
}
// all props
if propName == "" {
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 (ec *ExecutionContext) DispatchEvent(
ctx context.Context,
objectID runtime.RemoteObjectID,
eventName string,
) (values.Boolean, error) {
args := runtime.NewEvaluateArgs(PrepareEval(fmt.Sprintf(`
return new window.MouseEvent('%s', { bubbles: true, cancelable: true })
`, eventName)))
if ec.contextID != EmptyExecutionContextID {
args.SetContextID(ec.contextID)
}
evt, err := ec.client.Runtime.Evaluate(ctx, args)
if err != nil {
return values.False, nil
}
if evt.ExceptionDetails != nil {
return values.False, evt.ExceptionDetails
}
if evt.Result.ObjectID == nil {
return values.False, nil
}
evtID := evt.Result.ObjectID
// release the event object
defer ec.client.Runtime.ReleaseObject(ctx, runtime.NewReleaseObjectArgs(*evtID))
_, err = ec.CallMethod(
ctx,
objectID,
"dispatchEvent",
[]runtime.CallArgument{
{
ObjectID: evt.Result.ObjectID,
},
},
)
if err != nil {
return values.False, err
}
return values.True, nil
}
func (ec *ExecutionContext) CallFunction(ctx context.Context, declaration string, args ...runtime.CallArgument) (runtime.RemoteObject, error) {
cfArgs := runtime.NewCallFunctionOnArgs(declaration).SetArguments(args)
if ec.contextID != EmptyExecutionContextID {
cfArgs.SetExecutionContextID(ec.contextID)
}
repl, err := ec.client.Runtime.CallFunctionOn(ctx, cfArgs)
if err != nil {
return runtime.RemoteObject{}, err
}
if repl.ExceptionDetails != nil {
exception := *repl.ExceptionDetails
return runtime.RemoteObject{}, errors.New(exception.Error())
}
return repl.Result, nil
}
func (ec *ExecutionContext) evalWithValueInternal(ctx context.Context, args *runtime.EvaluateArgs) (core.Value, error) {
obj, err := ec.evalInternal(ctx, args)
if err != nil {
return values.None, err
}
if obj.Type != "undefined" && obj.Type != "null" {
return values.Unmarshal(obj.Value)
}
return Unmarshal(&obj)
}
func (ec *ExecutionContext) evalInternal(ctx context.Context, args *runtime.EvaluateArgs) (runtime.RemoteObject, error) {
if ec.contextID != EmptyExecutionContextID {
args.SetContextID(ec.contextID)
}
out, err := ec.client.Runtime.Evaluate(ctx, args)
if err != nil {
return runtime.RemoteObject{}, err
}
if out.ExceptionDetails != nil {
ex := out.ExceptionDetails
return runtime.RemoteObject{}, core.Error(
core.ErrUnexpected,
fmt.Sprintf("%s: %s", ex.Text, *ex.Exception.Description),
)
}
return out.Result, nil
}

View File

@ -1,160 +0,0 @@
package eval
import (
"context"
"encoding/json"
"fmt"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/runtime"
)
func PrepareEval(exp string) string {
return fmt.Sprintf("((function () {%s})())", exp)
}
func Eval(ctx context.Context, client *cdp.Client, exp string, ret bool, async bool) (core.Value, error) {
args := runtime.
NewEvaluateArgs(PrepareEval(exp)).
SetReturnByValue(ret).
SetAwaitPromise(async)
out, err := client.Runtime.Evaluate(ctx, args)
if err != nil {
return values.None, err
}
if out.ExceptionDetails != nil {
ex := out.ExceptionDetails
return values.None, core.Error(
core.ErrUnexpected,
fmt.Sprintf("%s: %s", ex.Text, *ex.Exception.Description),
)
}
if out.Result.Type != "undefined" {
return values.Unmarshal(out.Result.Value)
}
return Unmarshal(&out.Result)
}
func Property(
ctx context.Context,
client *cdp.Client,
objectID runtime.RemoteObjectID,
propName string,
) (core.Value, error) {
res, err := client.Runtime.GetProperties(
ctx,
runtime.NewGetPropertiesArgs(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 Method(
ctx context.Context,
client *cdp.Client,
objectID runtime.RemoteObjectID,
methodName string,
args []runtime.CallArgument,
) (*runtime.RemoteObject, error) {
found, err := client.Runtime.CallFunctionOn(
ctx,
runtime.NewCallFunctionOnArgs(methodName).
SetObjectID(objectID).
SetArguments(args),
)
if err != nil {
return nil, err
}
if found.ExceptionDetails != nil {
return nil, found.ExceptionDetails
}
if found.Result.ObjectID == nil {
return nil, nil
}
return &found.Result, nil
}
func MethodQuerySelector(
ctx context.Context,
client *cdp.Client,
objectID runtime.RemoteObjectID,
selector string,
) (runtime.RemoteObjectID, error) {
bytes, err := json.Marshal(selector)
if err != nil {
return "", err
}
obj, err := Method(ctx, client, objectID, "querySelector", []runtime.CallArgument{
{
Value: json.RawMessage(bytes),
},
})
if err != nil {
return "", err
}
if obj.ObjectID == nil {
return "", nil
}
return *obj.ObjectID, 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
}

View File

@ -0,0 +1,35 @@
package eval
import (
"fmt"
"strconv"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp/protocol/runtime"
)
func PrepareEval(exp string) string {
return fmt.Sprintf("((function () {%s})())", exp)
}
func Unmarshal(obj *runtime.RemoteObject) (core.Value, error) {
if obj == nil {
return values.None, nil
}
switch obj.Type {
case "string":
str, err := strconv.Unquote(string(obj.Value))
if err != nil {
return values.None, err
}
return values.NewString(str), nil
case "undefined", "null":
return values.None, nil
default:
return values.Unmarshal(obj.Value)
}
}

View File

@ -1,9 +1,10 @@
package eval package eval
import ( import (
"strconv"
"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"
"strconv"
) )
func Param(input core.Value) string { func Param(input core.Value) string {

View File

@ -5,11 +5,11 @@ import (
"reflect" "reflect"
"sync" "sync"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/page" "github.com/mafredri/cdp/protocol/page"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
) )
type ( type (
@ -179,6 +179,16 @@ func (broker *EventBroker) Close() error {
return nil return nil
} }
func (broker *EventBroker) StopAndClose() error {
err := broker.Stop()
if err != nil {
return err
}
return broker.Close()
}
func (broker *EventBroker) runLoop(ctx context.Context) { func (broker *EventBroker) runLoop(ctx context.Context) {
for { for {
select { select {
@ -265,6 +275,7 @@ func (broker *EventBroker) emit(ctx context.Context, event Event, message interf
listeners, ok := broker.listeners[event] listeners, ok := broker.listeners[event]
if !ok { if !ok {
broker.mu.Unlock()
return return
} }

View File

@ -269,7 +269,7 @@ func TestEventBroker(t *testing.T) {
var listener events.EventListener var listener events.EventListener
listener = func(ctx context.Context, message interface{}) { listener = func(ctx context.Context, message interface{}) {
counter += 1 counter++
b.RemoveEventListener(events.EventLoad, listener) b.RemoveEventListener(events.EventLoad, listener)
} }

View File

@ -1,56 +0,0 @@
package events
import (
"context"
"fmt"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/runtime"
)
func DispatchEvent(
ctx context.Context,
client *cdp.Client,
objectID runtime.RemoteObjectID,
eventName string,
) (values.Boolean, error) {
evt, err := client.Runtime.Evaluate(ctx, runtime.NewEvaluateArgs(eval.PrepareEval(fmt.Sprintf(`
return new window.MouseEvent('%s', { bubbles: true, cancelable: true })
`, eventName))))
if err != nil {
return values.False, nil
}
if evt.ExceptionDetails != nil {
return values.False, evt.ExceptionDetails
}
if evt.Result.ObjectID == nil {
return values.False, nil
}
evtID := evt.Result.ObjectID
// release the event object
defer client.Runtime.ReleaseObject(ctx, runtime.NewReleaseObjectArgs(*evtID))
_, err = eval.Method(
ctx,
client,
objectID,
"dispatchEvent",
[]runtime.CallArgument{
{
ObjectID: evt.Result.ObjectID,
},
},
)
if err != nil {
return values.False, err
}
return values.True, nil
}

View File

@ -0,0 +1,126 @@
package events
import (
"context"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/page"
"github.com/pkg/errors"
)
func WaitForLoadEvent(ctx context.Context, client *cdp.Client) error {
loadEventFired, err := client.Page.LoadEventFired(ctx)
if err != nil {
return errors.Wrap(err, "failed to create load event hook")
}
_, err = loadEventFired.Recv()
if err != nil {
return err
}
return loadEventFired.Close()
}
func CreateEventBroker(client *cdp.Client) (*EventBroker, error) {
var err error
var onLoad page.LoadEventFiredClient
var onReload dom.DocumentUpdatedClient
var onAttrModified dom.AttributeModifiedClient
var onAttrRemoved dom.AttributeRemovedClient
var onChildCountUpdated dom.ChildNodeCountUpdatedClient
var onChildNodeInserted dom.ChildNodeInsertedClient
var onChildNodeRemoved dom.ChildNodeRemovedClient
ctx := context.Background()
onLoad, err = client.Page.LoadEventFired(ctx)
if err != nil {
return nil, err
}
onReload, err = client.DOM.DocumentUpdated(ctx)
if err != nil {
onLoad.Close()
return nil, err
}
onAttrModified, err = client.DOM.AttributeModified(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
return nil, err
}
onAttrRemoved, err = client.DOM.AttributeRemoved(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
return nil, err
}
onChildCountUpdated, err = client.DOM.ChildNodeCountUpdated(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
return nil, err
}
onChildNodeInserted, err = client.DOM.ChildNodeInserted(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
onChildCountUpdated.Close()
return nil, err
}
onChildNodeRemoved, err = client.DOM.ChildNodeRemoved(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
onChildCountUpdated.Close()
onChildNodeInserted.Close()
return nil, err
}
broker := NewEventBroker(
onLoad,
onReload,
onAttrModified,
onAttrRemoved,
onChildCountUpdated,
onChildNodeInserted,
onChildNodeRemoved,
)
err = broker.Start()
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
onChildCountUpdated.Close()
onChildNodeInserted.Close()
onChildNodeRemoved.Close()
return nil, err
}
return broker, nil
}

View File

@ -2,12 +2,12 @@ package events
import ( import (
"context" "context"
"time"
"github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"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"
"time"
) )
type ( type (
@ -58,18 +58,15 @@ func (task *WaitTask) Run(ctx context.Context) (core.Value, error) {
} }
func NewEvalWaitTask( func NewEvalWaitTask(
client *cdp.Client, ec *eval.ExecutionContext,
predicate string, predicate string,
polling time.Duration, polling time.Duration,
) *WaitTask { ) *WaitTask {
return NewWaitTask( return NewWaitTask(
func(ctx context.Context) (core.Value, error) { func(ctx context.Context) (core.Value, error) {
return eval.Eval( return ec.EvalWithValue(
ctx, ctx,
client,
predicate, predicate,
true,
false,
) )
}, },
polling, polling,

View File

@ -4,16 +4,15 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"math" "golang.org/x/net/html"
"strings" "strings"
"time" "time"
"github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
"github.com/MontFerret/ferret/pkg/drivers/common" "github.com/MontFerret/ferret/pkg/drivers/common"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/mafredri/cdp" "github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/dom"
@ -27,11 +26,6 @@ var emptyExpires = time.Time{}
type ( type (
batchFunc = func() error batchFunc = func() error
Quad struct {
X float64
Y float64
}
) )
func runBatch(funcs ...batchFunc) error { func runBatch(funcs ...batchFunc) error {
@ -44,101 +38,6 @@ func runBatch(funcs ...batchFunc) error {
return eg.Wait() return eg.Wait()
} }
func getRootElement(ctx context.Context, client *cdp.Client) (*dom.GetDocumentReply, error) {
d, err := client.DOM.GetDocument(ctx, dom.NewGetDocumentArgs().SetDepth(1))
if err != nil {
return nil, err
}
return d, nil
}
func fromProtocolQuad(quad dom.Quad) []Quad {
return []Quad{
{
X: quad[0],
Y: quad[1],
},
{
X: quad[2],
Y: quad[3],
},
{
X: quad[4],
Y: quad[5],
},
{
X: quad[6],
Y: quad[7],
},
}
}
func computeQuadArea(quads []Quad) float64 {
var area float64
for i := range quads {
p1 := quads[i]
p2 := quads[(i+1)%len(quads)]
area += (p1.X*p2.Y - p2.X*p1.Y) / 2
}
return math.Abs(area)
}
func getClickablePoint(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (Quad, error) {
qargs := dom.NewGetContentQuadsArgs()
switch {
case id.objectID != "":
qargs.SetObjectID(id.objectID)
case id.backendID != 0:
qargs.SetBackendNodeID(id.backendID)
default:
qargs.SetNodeID(id.nodeID)
}
res, err := client.DOM.GetContentQuads(ctx, qargs)
if err != nil {
return Quad{}, err
}
if res.Quads == nil || len(res.Quads) == 0 {
return Quad{}, errors.New("node is either not visible or not an HTMLElement")
}
quads := make([][]Quad, 0, len(res.Quads))
for _, q := range res.Quads {
quad := fromProtocolQuad(q)
if computeQuadArea(quad) > 1 {
quads = append(quads, quad)
}
}
if len(quads) == 0 {
return Quad{}, errors.New("node is either not visible or not an HTMLElement")
}
// Return the middle point of the first quad.
quad := quads[0]
var x float64
var y float64
for _, q := range quad {
x += q.X
y += q.Y
}
return Quad{
X: x / 4,
Y: y / 4,
}, nil
}
func parseAttrs(attrs []string) *values.Object { func parseAttrs(attrs []string) *values.Object {
var attr values.String var attr values.String
@ -167,25 +66,14 @@ func parseAttrs(attrs []string) *values.Object {
return res return res
} }
func loadInnerHTML(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (values.String, error) { func loadInnerHTML(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, nodeType html.NodeType) (values.String, error) {
// not a document
if nodeType != html.DocumentNode {
var objID runtime.RemoteObjectID var objID runtime.RemoteObjectID
switch { if id.objectID != "" {
case id.objectID != "":
objID = id.objectID objID = id.objectID
case id.backendID > 0: } else {
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetBackendNodeID(id.backendID))
if err != nil {
return "", err
}
if repl.Object.ObjectID == nil {
return "", errors.New("unable to resolve node")
}
objID = *repl.Object.ObjectID
default:
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID)) repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID))
if err != nil { if err != nil {
@ -199,45 +87,44 @@ func loadInnerHTML(ctx context.Context, client *cdp.Client, id *HTMLElementIdent
objID = *repl.Object.ObjectID objID = *repl.Object.ObjectID
} }
res, err := exec.ReadProperty(ctx, objID, "innerHTML")
if err != nil {
return "", err
}
return values.NewString(res.String()), nil
}
repl, err := exec.EvalWithValue(ctx, "return document.documentElement.innerHTML")
if err != nil {
return "", err
}
return values.NewString(repl.String()), nil
}
func loadInnerHTMLByNodeID(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, nodeID dom.NodeID) (values.String, error) {
node, err := client.DOM.DescribeNode(ctx, dom.NewDescribeNodeArgs().SetNodeID(nodeID))
if err != nil {
return values.EmptyString, err
}
return loadInnerHTML(ctx, client, exec, HTMLElementIdentity{
nodeID: nodeID,
}, common.ToHTMLType(node.Node.NodeType))
}
func loadInnerText(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, id HTMLElementIdentity, nodeType html.NodeType) (values.String, error) {
// not a document // not a document
if id.nodeID != 1 { if nodeType != html.DocumentNode {
res, err := eval.Property(ctx, client, objID, "innerHTML")
if err != nil {
return "", err
}
return values.NewString(res.String()), err
}
repl, err := client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetObjectID(objID))
if err != nil {
return "", err
}
return values.NewString(repl.OuterHTML), nil
}
func loadInnerText(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (values.String, error) {
var objID runtime.RemoteObjectID var objID runtime.RemoteObjectID
switch { if id.objectID != "" {
case id.objectID != "":
objID = id.objectID objID = id.objectID
case id.backendID > 0: } else {
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetBackendNodeID(id.backendID))
if err != nil {
return "", err
}
if repl.Object.ObjectID == nil {
return "", errors.New("unable to resolve node")
}
objID = *repl.Object.ObjectID
default:
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID)) repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID))
if err != nil { if err != nil {
@ -251,9 +138,7 @@ func loadInnerText(ctx context.Context, client *cdp.Client, id *HTMLElementIdent
objID = *repl.Object.ObjectID objID = *repl.Object.ObjectID
} }
// not a document res, err := exec.ReadProperty(ctx, objID, "innerText")
if id.nodeID != 1 {
res, err := eval.Property(ctx, client, objID, "innerText")
if err != nil { if err != nil {
return "", err return "", err
@ -262,15 +147,27 @@ func loadInnerText(ctx context.Context, client *cdp.Client, id *HTMLElementIdent
return values.NewString(res.String()), err return values.NewString(res.String()), err
} }
repl, err := client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetObjectID(objID)) repl, err := exec.EvalWithValue(ctx, "return document.documentElement.innerText")
if err != nil { if err != nil {
return "", err return "", err
} }
return parseInnerText(repl.OuterHTML) return values.NewString(repl.String()), nil
} }
//func loadInnerTextByNodeID(ctx context.Context, client *cdp.Client, exec *eval.ExecutionContext, nodeID dom.NodeID) (values.String, error) {
// node, err := client.DOM.DescribeNode(ctx, dom.NewDescribeNodeArgs().SetNodeID(nodeID))
//
// if err != nil {
// return values.EmptyString, err
// }
//
// return loadInnerText(ctx, client, exec, HTMLElementIdentity{
// nodeID: nodeID,
// }, common.ToHTMLType(node.Node.NodeType))
//}
func parseInnerText(innerHTML string) (values.String, error) { func parseInnerText(innerHTML string) (values.String, error) {
buff := bytes.NewBuffer([]byte(innerHTML)) buff := bytes.NewBuffer([]byte(innerHTML))
@ -283,135 +180,19 @@ func parseInnerText(innerHTML string) (values.String, error) {
return values.NewString(parsed.Text()), nil return values.NewString(parsed.Text()), nil
} }
func createChildrenArray(nodes []dom.Node) []*HTMLElementIdentity { func createChildrenArray(nodes []dom.Node) []HTMLElementIdentity {
children := make([]*HTMLElementIdentity, len(nodes)) children := make([]HTMLElementIdentity, len(nodes))
for idx, child := range nodes { for idx, child := range nodes {
children[idx] = &HTMLElementIdentity{ child := child
children[idx] = HTMLElementIdentity{
nodeID: child.NodeID, nodeID: child.NodeID,
backendID: child.BackendNodeID,
} }
} }
return children return children
} }
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 createEventBroker(client *cdp.Client) (*events.EventBroker, error) {
var err error
var onLoad page.LoadEventFiredClient
var onReload dom.DocumentUpdatedClient
var onAttrModified dom.AttributeModifiedClient
var onAttrRemoved dom.AttributeRemovedClient
var onChildCountUpdated dom.ChildNodeCountUpdatedClient
var onChildNodeInserted dom.ChildNodeInsertedClient
var onChildNodeRemoved dom.ChildNodeRemovedClient
ctx := context.Background()
onLoad, err = client.Page.LoadEventFired(ctx)
if err != nil {
return nil, err
}
onReload, err = client.DOM.DocumentUpdated(ctx)
if err != nil {
onLoad.Close()
return nil, err
}
onAttrModified, err = client.DOM.AttributeModified(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
return nil, err
}
onAttrRemoved, err = client.DOM.AttributeRemoved(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
return nil, err
}
onChildCountUpdated, err = client.DOM.ChildNodeCountUpdated(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
return nil, err
}
onChildNodeInserted, err = client.DOM.ChildNodeInserted(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
onChildCountUpdated.Close()
return nil, err
}
onChildNodeRemoved, err = client.DOM.ChildNodeRemoved(ctx)
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
onChildCountUpdated.Close()
onChildNodeInserted.Close()
return nil, err
}
broker := events.NewEventBroker(
onLoad,
onReload,
onAttrModified,
onAttrRemoved,
onChildCountUpdated,
onChildNodeInserted,
onChildNodeRemoved,
)
err = broker.Start()
if err != nil {
onLoad.Close()
onReload.Close()
onAttrModified.Close()
onAttrRemoved.Close()
onChildCountUpdated.Close()
onChildNodeInserted.Close()
onChildNodeRemoved.Close()
return nil, err
}
return broker, nil
}
func fromDriverCookie(url string, cookie drivers.HTTPCookie) network.CookieParam { func fromDriverCookie(url string, cookie drivers.HTTPCookie) network.CookieParam {
sameSite := network.CookieSameSiteNotSet sameSite := network.CookieSameSiteNotSet
@ -485,9 +266,58 @@ func normalizeCookieURL(url string) string {
return httpPrefix + url return httpPrefix + url
} }
func randomDuration(delay values.Int) time.Duration { func resolveFrame(ctx context.Context, client *cdp.Client, frame page.Frame) (dom.Node, runtime.ExecutionContextID, error) {
max, min := core.NumberBoundaries(float64(int64(delay))) worldRepl, err := client.Page.CreateIsolatedWorld(ctx, page.NewCreateIsolatedWorldArgs(frame.ID))
value := core.Random(max, min)
return time.Duration(int64(value)) if err != nil {
return dom.Node{}, -1, err
}
evalRes, err := client.Runtime.Evaluate(
ctx,
runtime.NewEvaluateArgs(eval.PrepareEval("return document")).
SetContextID(worldRepl.ExecutionContextID),
)
if err != nil {
return dom.Node{}, -1, err
}
if evalRes.ExceptionDetails != nil {
exception := *evalRes.ExceptionDetails
return dom.Node{}, -1, errors.New(exception.Text)
}
if evalRes.Result.ObjectID == nil {
return dom.Node{}, -1, errors.New("failed to resolve frame document")
}
req, err := client.DOM.RequestNode(ctx, dom.NewRequestNodeArgs(*evalRes.Result.ObjectID))
if err != nil {
return dom.Node{}, -1, err
}
if req.NodeID == 0 {
return dom.Node{}, -1, errors.New("framed document is resolved with empty node id")
}
desc, err := client.DOM.DescribeNode(
ctx,
dom.
NewDescribeNodeArgs().
SetNodeID(req.NodeID).
SetDepth(1),
)
if err != nil {
return dom.Node{}, -1, err
}
// Returned node, by some reason, does not contain the NodeID
// So, we have to set it manually
desc.Node.NodeID = req.NodeID
return desc.Node, worldRepl.ExecutionContextID, nil
} }

View File

@ -0,0 +1,14 @@
package input
import (
"time"
"github.com/MontFerret/ferret/pkg/runtime/core"
)
func randomDuration(delay int) time.Duration {
max, min := core.NumberBoundaries(float64(delay))
value := core.Random(max, min)
return time.Duration(int64(value))
}

View File

@ -0,0 +1,52 @@
package input
import (
"context"
"time"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/input"
)
type Keyboard struct {
client *cdp.Client
}
func NewKeyboard(client *cdp.Client) *Keyboard {
return &Keyboard{client}
}
func (k *Keyboard) Down(ctx context.Context, char string) error {
return k.client.Input.DispatchKeyEvent(
ctx,
input.NewDispatchKeyEventArgs("keyDown").
SetText(char),
)
}
func (k *Keyboard) Up(ctx context.Context, char string) error {
return k.client.Input.DispatchKeyEvent(
ctx,
input.NewDispatchKeyEventArgs("keyUp").
SetText(char),
)
}
func (k *Keyboard) Type(ctx context.Context, text string, delay int) error {
for _, ch := range text {
ch := string(ch)
if err := k.Down(ctx, ch); err != nil {
return err
}
releaseDelay := randomDuration(delay)
time.Sleep(releaseDelay)
if err := k.Up(ctx, ch); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,305 @@
package input
import (
"context"
"fmt"
"time"
"github.com/gofrs/uuid"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
)
type Manager struct {
client *cdp.Client
exec *eval.ExecutionContext
keyboard *Keyboard
mouse *Mouse
}
func NewManager(
client *cdp.Client,
exec *eval.ExecutionContext,
keyboard *Keyboard,
mouse *Mouse,
) *Manager {
return &Manager{
client,
exec,
keyboard,
mouse,
}
}
func (m *Manager) Keyboard() *Keyboard {
return m.keyboard
}
func (m *Manager) Mouse() *Mouse {
return m.mouse
}
func (m *Manager) Scroll(ctx context.Context, x, y values.Float) error {
return m.exec.Eval(ctx, fmt.Sprintf(`
window.scrollBy({
top: %s,
left: %s,
behavior: 'instant'
});
`,
eval.ParamFloat(float64(x)),
eval.ParamFloat(float64(y)),
))
}
func (m *Manager) ScrollIntoViewBySelector(ctx context.Context, selector values.String) error {
return m.exec.Eval(ctx, 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()),
))
}
func (m *Manager) ScrollIntoViewByNodeID(ctx context.Context, nodeID dom.NodeID) error {
var attrID = "data-ferret-scroll"
id, err := uuid.NewV4()
if err != nil {
return err
}
err = m.client.DOM.SetAttributeValue(ctx, dom.NewSetAttributeValueArgs(nodeID, attrID, id.String()))
if err != nil {
return err
}
err = m.exec.Eval(
ctx,
fmt.Sprintf(`
var el = document.querySelector('[%s="%s"]');
if (el == null) {
throw new Error('element not found');
}
el.scrollIntoView({
behavior: 'instant',
inline: 'center',
block: 'center'
});
`,
attrID,
id.String(),
))
if err != nil {
return err
}
err = m.client.DOM.RemoveAttribute(ctx, dom.NewRemoveAttributeArgs(nodeID, attrID))
return err
}
func (m *Manager) ScrollTop(ctx context.Context) error {
return m.exec.Eval(ctx, `
window.scrollTo({
left: 0,
top: 0,
behavior: 'instant'
});
`)
}
func (m *Manager) ScrollBottom(ctx context.Context) error {
return m.exec.Eval(ctx, `
window.scrollTo({
left: 0,
top: window.document.body.scrollHeight,
behavior: 'instant'
});
`)
}
func (m *Manager) MoveMouseBySelector(ctx context.Context, parentNodeID dom.NodeID, selector values.String) error {
found, err := m.client.DOM.QuerySelector(ctx, dom.NewQuerySelectorArgs(parentNodeID, selector.String()))
if err != nil {
return err
}
return m.MoveMouseByNodeID(ctx, found.NodeID)
}
func (m *Manager) MoveMouseByNodeID(ctx context.Context, nodeID dom.NodeID) error {
err := m.ScrollIntoViewByNodeID(ctx, nodeID)
if err != nil {
return err
}
q, err := GetClickablePointByNodeID(ctx, m.client, nodeID)
if err != nil {
return err
}
return m.mouse.Move(ctx, q.X, q.Y)
}
func (m *Manager) MoveMouse(ctx context.Context, x, y values.Float) error {
return m.mouse.Move(ctx, float64(x), float64(y))
}
func (m *Manager) ClickBySelector(ctx context.Context, parentNodeID dom.NodeID, selector values.String) error {
found, err := m.client.DOM.QuerySelector(ctx, dom.NewQuerySelectorArgs(parentNodeID, selector.String()))
if err != nil {
return err
}
return m.ClickByNodeID(ctx, found.NodeID)
}
func (m *Manager) ClickByNodeID(ctx context.Context, nodeID dom.NodeID) error {
if err := m.ScrollIntoViewByNodeID(ctx, nodeID); err != nil {
return err
}
points, err := GetClickablePointByNodeID(ctx, m.client, nodeID)
if err != nil {
return err
}
if err := m.mouse.Click(ctx, points.X, points.Y, 50); err != nil {
return nil
}
return nil
}
func (m *Manager) TypeBySelector(ctx context.Context, parentNodeID dom.NodeID, selector values.String, text core.Value, delay values.Int) error {
found, err := m.client.DOM.QuerySelector(ctx, dom.NewQuerySelectorArgs(parentNodeID, selector.String()))
if err != nil {
return err
}
return m.TypeByNodeID(ctx, found.NodeID, text, delay)
}
func (m *Manager) TypeByNodeID(ctx context.Context, nodeID dom.NodeID, text core.Value, delay values.Int) error {
if err := m.ScrollIntoViewByNodeID(ctx, nodeID); err != nil {
return err
}
if err := m.client.DOM.Focus(ctx, dom.NewFocusArgs().SetNodeID(nodeID)); err != nil {
return err
}
_, min := core.NumberBoundaries(float64(delay))
beforeTypeDelay := time.Duration(min)
time.Sleep(beforeTypeDelay * time.Millisecond)
return m.keyboard.Type(ctx, text.String(), int(delay))
}
func (m *Manager) SelectBySelector(ctx context.Context, parentNodeID dom.NodeID, selector values.String, value *values.Array) (*values.Array, error) {
found, err := m.client.DOM.QuerySelector(ctx, dom.NewQuerySelectorArgs(parentNodeID, selector.String()))
if err != nil {
return nil, err
}
return m.SelectByNodeID(ctx, found.NodeID, value)
}
func (m *Manager) SelectByNodeID(ctx context.Context, nodeID dom.NodeID, value *values.Array) (*values.Array, error) {
if err := m.ScrollIntoViewByNodeID(ctx, nodeID); err != nil {
return nil, err
}
if err := m.client.DOM.Focus(ctx, dom.NewFocusArgs().SetNodeID(nodeID)); err != nil {
return nil, err
}
var attrID = "data-ferret-select"
id, err := uuid.NewV4()
if err != nil {
return nil, err
}
err = m.client.DOM.SetAttributeValue(ctx, dom.NewSetAttributeValueArgs(nodeID, attrID, id.String()))
if err != nil {
return nil, err
}
res, err := m.exec.EvalWithValue(
ctx,
fmt.Sprintf(`
var el = document.querySelector('[%s="%s"]');
if (el == null) {
return [];
}
var values = %s;
if (el.nodeName.toLowerCase() !== 'select') {
throw new Error('element is not a <select> element.');
}
var options = Array.from(el.options);
el.value = undefined;
for (var option of options) {
option.selected = values.includes(option.value);
if (option.selected && !el.multiple) {
break;
}
}
el.dispatchEvent(new Event('input', { 'bubbles': true }));
el.dispatchEvent(new Event('change', { 'bubbles': true }));
return options.filter(option => option.selected).map(option => option.value);
`,
attrID,
id.String(),
value.String(),
),
)
if err != nil {
return nil, err
}
err = m.client.DOM.RemoveAttribute(ctx, dom.NewRemoveAttributeArgs(nodeID, attrID))
if err != nil {
return nil, err
}
arr, ok := res.(*values.Array)
if ok {
return arr, nil
}
return nil, core.TypeError(types.Array, res.Type())
}

View File

@ -0,0 +1,83 @@
package input
import (
"context"
"time"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/input"
)
type Mouse struct {
client *cdp.Client
x float64
y float64
}
func NewMouse(client *cdp.Client) *Mouse {
return &Mouse{client, 0, 0}
}
func (m *Mouse) Click(ctx context.Context, x, y float64, delay int) error {
if err := m.Move(ctx, x, y); err != nil {
return err
}
if err := m.Down(ctx, "left"); err != nil {
return err
}
releaseDelay := randomDuration(delay)
time.Sleep(releaseDelay * time.Millisecond)
return m.Up(ctx, "left")
}
func (m *Mouse) Down(ctx context.Context, button string) error {
return m.client.Input.DispatchMouseEvent(
ctx,
input.NewDispatchMouseEventArgs("mousePressed", m.x, m.y).
SetClickCount(1).
SetButton(button),
)
}
func (m *Mouse) Up(ctx context.Context, button string) error {
return m.client.Input.DispatchMouseEvent(
ctx,
input.NewDispatchMouseEventArgs("mouseReleased", m.x, m.y).
SetClickCount(1).
SetButton(button),
)
}
func (m *Mouse) Move(ctx context.Context, x, y float64) error {
return m.MoveBySteps(ctx, x, y, 1)
}
func (m *Mouse) MoveBySteps(ctx context.Context, x, y float64, steps int) error {
fromX := m.x
fromY := m.y
for i := 0; i <= steps; i++ {
iFloat := float64(i)
stepFloat := float64(steps)
toX := fromX + (x-fromX)*(iFloat/stepFloat)
toY := fromY + (y-fromY)*(iFloat/stepFloat)
err := m.client.Input.DispatchMouseEvent(
ctx,
input.NewDispatchMouseEventArgs("mouseMoved", toX, toY),
)
if err != nil {
return err
}
}
m.x = x
m.y = y
return nil
}

View File

@ -0,0 +1,124 @@
package input
import (
"context"
"math"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/runtime"
"github.com/pkg/errors"
)
type Quad struct {
X float64
Y float64
}
func fromProtocolQuad(quad dom.Quad) []Quad {
return []Quad{
{
X: quad[0],
Y: quad[1],
},
{
X: quad[2],
Y: quad[3],
},
{
X: quad[4],
Y: quad[5],
},
{
X: quad[6],
Y: quad[7],
},
}
}
func computeQuadArea(quads []Quad) float64 {
var area float64
for i := range quads {
p1 := quads[i]
p2 := quads[(i+1)%len(quads)]
area += (p1.X*p2.Y - p2.X*p1.Y) / 2
}
return math.Abs(area)
}
func intersectQuadWithViewport(quad []Quad, width, height float64) []Quad {
quads := make([]Quad, 0, len(quad))
for _, point := range quad {
quads = append(quads, Quad{
X: math.Min(math.Max(point.X, 0), width),
Y: math.Min(math.Max(point.Y, 0), height),
})
}
return quads
}
func getClickablePoint(ctx context.Context, client *cdp.Client, qargs *dom.GetContentQuadsArgs) (Quad, error) {
contentQuadsReply, err := client.DOM.GetContentQuads(ctx, qargs)
if err != nil {
return Quad{}, err
}
if contentQuadsReply.Quads == nil || len(contentQuadsReply.Quads) == 0 {
return Quad{}, errors.New("node is either not visible or not an HTMLElement")
}
layoutMetricsReply, err := client.Page.GetLayoutMetrics(ctx)
if err != nil {
return Quad{}, err
}
clientWidth := layoutMetricsReply.LayoutViewport.ClientWidth
clientHeight := layoutMetricsReply.LayoutViewport.ClientHeight
quads := make([][]Quad, 0, len(contentQuadsReply.Quads))
for _, q := range contentQuadsReply.Quads {
quad := intersectQuadWithViewport(fromProtocolQuad(q), float64(clientWidth), float64(clientHeight))
if computeQuadArea(quad) > 1 {
quads = append(quads, quad)
}
}
if len(quads) == 0 {
return Quad{}, errors.New("node is either not visible or not an HTMLElement")
}
// Return the middle point of the first quad.
quad := quads[0]
var x float64
var y float64
for _, q := range quad {
x += q.X
y += q.Y
}
return Quad{
X: x / 4,
Y: y / 4,
}, nil
}
func GetClickablePointByNodeID(ctx context.Context, client *cdp.Client, nodeID dom.NodeID) (Quad, error) {
return getClickablePoint(ctx, client, dom.NewGetContentQuadsArgs().SetNodeID(nodeID))
}
func GetClickablePointByObjectID(ctx context.Context, client *cdp.Client, objectID runtime.RemoteObjectID) (Quad, error) {
return getClickablePoint(ctx, client, dom.NewGetContentQuadsArgs().SetObjectID(objectID))
}
func GetClickablePointByBackendID(ctx context.Context, client *cdp.Client, backendID dom.BackendNodeID) (Quad, error) {
return getClickablePoint(ctx, client, dom.NewGetContentQuadsArgs().SetBackendNodeID(backendID))
}

778
pkg/drivers/cdp/page.go Normal file
View File

@ -0,0 +1,778 @@
package cdp
import (
"context"
"encoding/json"
"hash/fnv"
"sync"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/emulation"
"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"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
"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"
)
type HTMLPage struct {
mu sync.Mutex
closed values.Boolean
logger *zerolog.Logger
conn *rpcc.Conn
client *cdp.Client
events *events.EventBroker
mouse *input.Mouse
keyboard *input.Keyboard
document *HTMLDocument
frames *common.LazyValue
}
func handleLoadError(logger *zerolog.Logger, client *cdp.Client) {
err := client.Page.Close(context.Background())
if err != nil {
logger.Warn().Timestamp().Err(err).Msg("failed to close document on load error")
}
}
func LoadHTMLPage(
ctx context.Context,
conn *rpcc.Conn,
params drivers.OpenPageParams,
) (*HTMLPage, error) {
logger := logging.FromContext(ctx)
if conn == nil {
return nil, core.Error(core.ErrMissedArgument, "connection")
}
client := cdp.NewClient(conn)
err := runBatch(
func() error {
return client.Page.Enable(ctx)
},
func() error {
return client.Page.SetLifecycleEventsEnabled(
ctx,
page.NewSetLifecycleEventsEnabledArgs(true),
)
},
func() error {
return client.DOM.Enable(ctx)
},
func() error {
return client.Runtime.Enable(ctx)
},
func() error {
ua := common.GetUserAgent(params.UserAgent)
logger.
Debug().
Timestamp().
Str("user-agent", ua).
Msg("using User-Agent")
// do not use custom user agent
if ua == "" {
return nil
}
return client.Emulation.SetUserAgentOverride(
ctx,
emulation.NewSetUserAgentOverrideArgs(ua),
)
},
func() error {
return client.Network.Enable(ctx, network.NewEnableArgs())
},
)
if err != nil {
return nil, err
}
err = client.Page.SetBypassCSP(ctx, page.NewSetBypassCSPArgs(true))
if err != nil {
return nil, err
}
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, errors.Wrap(err, "failed to set cookies")
}
}
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, errors.Wrap(err, "failed to set headers")
}
}
if params.URL != BlankPageURL && params.URL != "" {
repl, err := client.Page.Navigate(ctx, page.NewNavigateArgs(params.URL))
if err != nil {
return nil, errors.Wrap(err, "failed to load the page")
}
if repl.ErrorText != nil {
return nil, errors.Wrapf(errors.New(*repl.ErrorText), "failed to load the page: %s", params.URL)
}
err = events.WaitForLoadEvent(ctx, client)
if err != nil {
handleLoadError(logger, client)
return nil, errors.Wrap(err, "failed to load the page")
}
}
broker, err := events.CreateEventBroker(client)
if err != nil {
handleLoadError(logger, client)
return nil, errors.Wrap(err, "failed to create event events")
}
mouse := input.NewMouse(client)
keyboard := input.NewKeyboard(client)
doc, err := LoadRootHTMLDocument(ctx, logger, client, broker, mouse, keyboard)
if err != nil {
broker.StopAndClose()
handleLoadError(logger, client)
return nil, errors.Wrap(err, "failed to load root element")
}
return NewHTMLPage(
logger,
conn,
client,
broker,
mouse,
keyboard,
doc,
), nil
}
func NewHTMLPage(
logger *zerolog.Logger,
conn *rpcc.Conn,
client *cdp.Client,
broker *events.EventBroker,
mouse *input.Mouse,
keyboard *input.Keyboard,
document *HTMLDocument,
) *HTMLPage {
p := new(HTMLPage)
p.closed = values.False
p.logger = logger
p.conn = conn
p.client = client
p.events = broker
p.mouse = mouse
p.keyboard = keyboard
p.document = document
p.frames = common.NewLazyValue(p.unfoldFrames)
broker.AddEventListener(events.EventLoad, p.handlePageLoad)
broker.AddEventListener(events.EventError, p.handleError)
return p
}
func (p *HTMLPage) MarshalJSON() ([]byte, error) {
p.mu.Lock()
defer p.mu.Unlock()
return p.document.MarshalJSON()
}
func (p *HTMLPage) Type() core.Type {
return drivers.HTMLPageType
}
func (p *HTMLPage) String() string {
p.mu.Lock()
defer p.mu.Unlock()
return p.document.GetURL().String()
}
func (p *HTMLPage) Compare(other core.Value) int64 {
p.mu.Lock()
defer p.mu.Unlock()
tc := drivers.Compare(p.Type(), other.Type())
if tc != 0 {
return tc
}
cdpPage, ok := other.(*HTMLPage)
if !ok {
return 1
}
return p.document.GetURL().Compare(cdpPage.GetURL())
}
func (p *HTMLPage) Unwrap() interface{} {
p.mu.Lock()
defer p.mu.Unlock()
return p
}
func (p *HTMLPage) Hash() uint64 {
p.mu.Lock()
defer p.mu.Unlock()
h := fnv.New64a()
h.Write([]byte("CDP"))
h.Write([]byte(p.Type().String()))
h.Write([]byte(":"))
h.Write([]byte(p.document.GetURL()))
return h.Sum64()
}
func (p *HTMLPage) Copy() core.Value {
return values.None
}
func (p *HTMLPage) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
return common.GetInPage(ctx, p, path)
}
func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
return common.SetInPage(ctx, p, path, value)
}
func (p *HTMLPage) Iterate(ctx context.Context) (core.Iterator, error) {
p.mu.Lock()
defer p.mu.Unlock()
return p.document.Iterate(ctx)
}
func (p *HTMLPage) Length() values.Int {
p.mu.Lock()
defer p.mu.Unlock()
return p.document.Length()
}
func (p *HTMLPage) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
p.closed = values.True
err := p.events.Stop()
if err != nil {
p.logger.Warn().
Timestamp().
Str("url", p.document.GetURL().String()).
Err(err).
Msg("failed to stop event events")
}
err = p.events.Close()
if err != nil {
p.logger.Warn().
Timestamp().
Str("url", p.document.GetURL().String()).
Err(err).
Msg("failed to close event events")
}
err = p.document.Close()
if err != nil {
p.logger.Warn().
Timestamp().
Str("url", p.document.GetURL().String()).
Err(err).
Msg("failed to close root document")
}
err = p.client.Page.Close(context.Background())
if err != nil {
p.logger.Warn().
Timestamp().
Str("url", p.document.GetURL().String()).
Err(err).
Msg("failed to close browser page")
}
return p.conn.Close()
}
func (p *HTMLPage) IsClosed() values.Boolean {
p.mu.Lock()
defer p.mu.Unlock()
return p.closed
}
func (p *HTMLPage) GetURL() values.String {
p.mu.Lock()
defer p.mu.Unlock()
return p.document.GetURL()
}
func (p *HTMLPage) GetMainFrame() drivers.HTMLDocument {
p.mu.Lock()
defer p.mu.Unlock()
return p.document
}
func (p *HTMLPage) GetFrames(ctx context.Context) (*values.Array, error) {
p.mu.Lock()
defer p.mu.Unlock()
res, err := p.frames.Read(ctx)
if err != nil {
return nil, err
}
return res.(*values.Array).Clone().(*values.Array), nil
}
func (p *HTMLPage) GetFrame(ctx context.Context, idx values.Int) (core.Value, error) {
p.mu.Lock()
defer p.mu.Unlock()
res, err := p.frames.Read(ctx)
if err != nil {
return nil, err
}
return res.(*values.Array).Get(idx), nil
}
func (p *HTMLPage) GetCookies(ctx context.Context) (*values.Array, error) {
p.mu.Lock()
defer p.mu.Unlock()
repl, err := p.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 (p *HTMLPage) SetCookies(ctx context.Context, cookies ...drivers.HTTPCookie) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(cookies) == 0 {
return nil
}
params := make([]network.CookieParam, 0, len(cookies))
for _, c := range cookies {
params = append(params, fromDriverCookie(p.document.GetURL().String(), c))
}
return p.client.Network.SetCookies(ctx, network.NewSetCookiesArgs(params))
}
func (p *HTMLPage) DeleteCookies(ctx context.Context, cookies ...drivers.HTTPCookie) error {
p.mu.Lock()
defer p.mu.Unlock()
if len(cookies) == 0 {
return nil
}
var err error
for _, c := range cookies {
err = p.client.Network.DeleteCookies(ctx, fromDriverCookieDelete(p.document.GetURL().String(), c))
if err != nil {
break
}
}
return err
}
func (p *HTMLPage) PrintToPDF(ctx context.Context, params drivers.PDFParams) (values.Binary, error) {
p.mu.Lock()
defer p.mu.Unlock()
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 := p.client.Page.PrintToPDF(ctx, args)
if err != nil {
return values.NewBinary([]byte{}), err
}
return values.NewBinary(reply.Data), nil
}
func (p *HTMLPage) CaptureScreenshot(ctx context.Context, params drivers.ScreenshotParams) (values.Binary, error) {
p.mu.Lock()
defer p.mu.Unlock()
metrics, err := p.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 := p.client.Page.CaptureScreenshot(ctx, &args)
if err != nil {
return values.NewBinary([]byte{}), err
}
return values.NewBinary(reply.Data), nil
}
func (p *HTMLPage) Navigate(ctx context.Context, url values.String) error {
p.mu.Lock()
defer p.mu.Unlock()
if url == "" {
url = BlankPageURL
}
repl, err := p.client.Page.Navigate(ctx, page.NewNavigateArgs(url.String()))
if err != nil {
return err
}
if repl.ErrorText != nil {
return errors.New(*repl.ErrorText)
}
return p.WaitForNavigation(ctx)
}
func (p *HTMLPage) NavigateBack(ctx context.Context, skip values.Int) (values.Boolean, error) {
p.mu.Lock()
defer p.mu.Unlock()
history, err := p.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 = p.client.Page.NavigateToHistoryEntry(ctx, page.NewNavigateToHistoryEntryArgs(prev.ID))
if err != nil {
return values.False, err
}
err = p.WaitForNavigation(ctx)
if err != nil {
return values.False, err
}
return values.True, nil
}
func (p *HTMLPage) NavigateForward(ctx context.Context, skip values.Int) (values.Boolean, error) {
p.mu.Lock()
defer p.mu.Unlock()
history, err := p.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 = p.client.Page.NavigateToHistoryEntry(ctx, page.NewNavigateToHistoryEntryArgs(next.ID))
if err != nil {
return values.False, err
}
err = p.WaitForNavigation(ctx)
if err != nil {
return values.False, err
}
return values.True, nil
}
func (p *HTMLPage) WaitForNavigation(ctx context.Context) error {
onEvent := make(chan struct{})
var once sync.Once
listener := func(_ context.Context, _ interface{}) {
once.Do(func() {
close(onEvent)
})
}
defer p.events.RemoveEventListener(events.EventLoad, listener)
p.events.AddEventListener(events.EventLoad, listener)
select {
case <-onEvent:
return nil
case <-ctx.Done():
return core.ErrTimeout
}
}
func (p *HTMLPage) handlePageLoad(ctx context.Context, _ interface{}) {
p.mu.Lock()
defer p.mu.Unlock()
nextDoc, err := LoadRootHTMLDocument(ctx, p.logger, p.client, p.events, p.mouse, p.keyboard)
if err != nil {
p.logger.Error().
Timestamp().
Err(err).
Msg("failed to load new root document after page load")
return
}
// close the prev document
err = p.document.Close()
if err != nil {
p.logger.Error().
Timestamp().
Err(err).
Msgf("failed to close root document: %s", p.document.GetURL())
}
// set the new root document
p.document = nextDoc
// reset all loaded frames
p.frames.Reset()
}
func (p *HTMLPage) handleError(_ context.Context, val interface{}) {
err, ok := val.(error)
if !ok {
return
}
p.logger.Error().
Timestamp().
Err(err).
Msg("unexpected error")
}
func (p *HTMLPage) unfoldFrames(ctx context.Context) (core.Value, error) {
res := values.NewArray(10)
err := common.CollectFrames(ctx, res, p.document)
if err != nil {
return nil, err
}
return res, nil
}

View File

@ -2,7 +2,9 @@ package templates
import ( import (
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
) )

View File

@ -2,11 +2,11 @@ package templates
import ( import (
"fmt" "fmt"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
"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/MontFerret/ferret/pkg/drivers"
) )
func WaitBySelectorAll(selector values.String, when drivers.WaitEvent, value core.Value, check string) string { func WaitBySelectorAll(selector values.String, when drivers.WaitEvent, value core.Value, check string) string {

View File

@ -0,0 +1,66 @@
package templates
const xPathTemplate = `
(element, expression) => {
const out = document.evaluate(
expression,
element,
null,
XPathResult.ANY_TYPE
);
let result;
switch (out.resultType) {
case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
case XPathResult.ORDERED_NODE_ITERATOR_TYPE: {
result = [];
let item;
while ((item = out.iterateNext())) {
result.push(item);
}
break;
}
case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE:
case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE: {
result = [];
for (let i = 0; i < out.snapshotLength; i++) {
const item = out.snapshotItem(i);
if (item != null) {
result.push(item);
}
}
break;
}
case XPathResult.NUMBER_TYPE: {
result = out.numberValue;
break;
}
case XPathResult.STRING_TYPE: {
result = out.stringValue;
break;
}
case XPathResult.BOOLEAN_TYPE: {
result = out.booleanValue;
break;
}
case XPathResult.ANY_UNORDERED_NODE_TYPE:
case XPathResult.FIRST_ORDERED_NODE_TYPE: {
result = out.singleNodeValue;
break;
}
default: {
break;
}
}
return result;
}
`
func XPath() string {
return xPathTemplate
}

View File

@ -0,0 +1,33 @@
package common
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
func CollectFrames(ctx context.Context, receiver *values.Array, doc drivers.HTMLDocument) error {
receiver.Push(doc)
children, err := doc.GetChildDocuments(ctx)
if err != nil {
return err
}
children.ForEach(func(value core.Value, idx int) bool {
childDoc, ok := value.(drivers.HTMLDocument)
if !ok {
err = core.TypeError(value.Type(), drivers.HTMLDocumentType)
return false
}
return CollectFrames(ctx, receiver, childDoc) == nil
})
return nil
}

View File

@ -9,9 +9,81 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values/types" "github.com/MontFerret/ferret/pkg/runtime/values/types"
) )
func GetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value) (core.Value, error) {
if len(path) == 0 {
return page, nil
}
segment := path[0]
if segment.Type() == types.String {
segment := segment.(values.String)
switch segment {
case "mainFrame", "document":
return GetInDocument(ctx, page.GetMainFrame(), path[1:])
case "frames":
if len(path) == 1 {
return page.GetFrames(ctx)
}
idx := path[1]
if !values.IsNumber(idx) {
return values.None, core.TypeError(idx.Type(), types.Int, types.Float)
}
value, err := page.GetFrame(ctx, values.ToInt(idx))
if err != nil {
return values.None, err
}
if len(path) == 2 {
return value, nil
}
frame, err := drivers.ToDocument(value)
if err != nil {
return values.None, err
}
return GetInDocument(ctx, frame, path[2:])
case "url", "URL":
return page.GetMainFrame().GetURL(), nil
case "cookies":
if len(path) == 1 {
return page.GetCookies(ctx)
}
switch idx := path[1].(type) {
case values.Int:
cookies, err := page.GetCookies(ctx)
if err != nil {
return values.None, err
}
return cookies.Get(idx), nil
default:
return values.None, core.TypeError(idx.Type(), types.Int)
}
case "isClosed":
return page.IsClosed(), nil
case "title":
return page.GetMainFrame().GetTitle(), nil
default:
return GetInDocument(ctx, page.GetMainFrame(), path)
}
}
return GetInDocument(ctx, page.GetMainFrame(), path)
}
func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Value) (core.Value, error) { func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Value) (core.Value, error) {
if path == nil || len(path) == 0 { if len(path) == 0 {
return values.None, nil return doc, nil
} }
segment := path[0] segment := path[0]
@ -22,38 +94,53 @@ func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Va
switch segment { switch segment {
case "url", "URL": case "url", "URL":
return doc.GetURL(), nil return doc.GetURL(), nil
case "cookies": case "title":
if len(path) == 1 { return doc.GetTitle(), nil
return doc.GetCookies(ctx) case "parent":
parent := doc.GetParentDocument()
if parent == nil {
return values.None, nil
} }
switch idx := path[1].(type) { if len(path) == 1 {
case values.Int: return parent, nil
cookies, err := doc.GetCookies(ctx) }
return GetInDocument(ctx, parent, path[1:])
case "body", "head":
out := doc.QuerySelector(ctx, segment)
if out == values.None {
return out, nil
}
if len(path) == 1 {
return out, nil
}
el, err := drivers.ToElement(out)
if err != nil { if err != nil {
return values.None, err return values.None, err
} }
return cookies.Get(idx), nil return GetInElement(ctx, el, path[1:])
case "innerHTML":
return doc.GetElement().GetInnerHTML(ctx), nil
case "innerText":
return doc.GetElement().GetInnerText(ctx), nil
default: default:
return values.None, core.TypeError(idx.Type(), types.Int) return GetInNode(ctx, doc.GetElement(), path)
}
case "body":
return doc.QuerySelector(ctx, "body"), nil
case "head":
return doc.QuerySelector(ctx, "head"), nil
default:
return GetInNode(ctx, doc.DocumentElement(), path)
} }
} }
return GetInNode(ctx, doc.DocumentElement(), path) return GetInNode(ctx, doc.GetElement(), path)
} }
func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value) (core.Value, error) { func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value) (core.Value, error) {
if path == nil || len(path) == 0 { if len(path) == 0 {
return values.None, nil return el, nil
} }
segment := path[0] segment := path[0]
@ -63,9 +150,9 @@ func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
switch segment { switch segment {
case "innerText": case "innerText":
return el.InnerText(ctx), nil return el.GetInnerText(ctx), nil
case "innerHTML": case "innerHTML":
return el.InnerHTML(ctx), nil return el.GetInnerHTML(ctx), nil
case "value": case "value":
return el.GetValue(ctx), nil return el.GetValue(ctx), nil
case "attributes": case "attributes":
@ -97,8 +184,8 @@ func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
} }
func GetInNode(ctx context.Context, node drivers.HTMLNode, path []core.Value) (core.Value, error) { func GetInNode(ctx context.Context, node drivers.HTMLNode, path []core.Value) (core.Value, error) {
if path == nil || len(path) == 0 { if len(path) == 0 {
return values.None, nil return node, nil
} }
nt := node.Type() nt := node.Type()
@ -118,10 +205,12 @@ func GetInNode(ctx context.Context, node drivers.HTMLNode, path []core.Value) (c
segment := segment.(values.String) segment := segment.(values.String)
switch segment { switch segment {
case "isDetached":
return node.IsDetached(), nil
case "nodeType": case "nodeType":
return node.NodeType(), nil return node.GetNodeType(), nil
case "nodeName": case "nodeName":
return node.NodeName(), nil return node.GetNodeName(), nil
case "children": case "children":
children := node.GetChildNodes(ctx) children := node.GetChildNodes(ctx)

View File

@ -12,7 +12,7 @@ type (
LazyValueFactory func(ctx context.Context) (core.Value, error) LazyValueFactory func(ctx context.Context) (core.Value, error)
LazyValue struct { LazyValue struct {
sync.Mutex mu sync.Mutex
factory LazyValueFactory factory LazyValueFactory
ready bool ready bool
value core.Value value core.Value
@ -32,8 +32,8 @@ func NewLazyValue(factory LazyValueFactory) *LazyValue {
// Ready indicates whether the value is ready. // Ready indicates whether the value is ready.
// @returns (Boolean) - Boolean value indicating whether the value is ready. // @returns (Boolean) - Boolean value indicating whether the value is ready.
func (lv *LazyValue) Ready() bool { func (lv *LazyValue) Ready() bool {
lv.Lock() lv.mu.Lock()
defer lv.Unlock() defer lv.mu.Unlock()
return lv.ready return lv.ready
} }
@ -42,8 +42,8 @@ func (lv *LazyValue) Ready() bool {
// Not thread safe. Should not mutated. // Not thread safe. Should not mutated.
// @returns (Value) - Underlying value if successfully loaded, otherwise error // @returns (Value) - Underlying value if successfully loaded, otherwise error
func (lv *LazyValue) Read(ctx context.Context) (core.Value, error) { func (lv *LazyValue) Read(ctx context.Context) (core.Value, error) {
lv.Lock() lv.mu.Lock()
defer lv.Unlock() defer lv.mu.Unlock()
if !lv.ready { if !lv.ready {
lv.load(ctx) lv.load(ctx)
@ -56,8 +56,8 @@ func (lv *LazyValue) Read(ctx context.Context) (core.Value, error) {
// Loads a value if it's not ready. // Loads a value if it's not ready.
// Thread safe. // Thread safe.
func (lv *LazyValue) Write(ctx context.Context, writer func(v core.Value, err error)) { func (lv *LazyValue) Write(ctx context.Context, writer func(v core.Value, err error)) {
lv.Lock() lv.mu.Lock()
defer lv.Unlock() defer lv.mu.Unlock()
if !lv.ready { if !lv.ready {
lv.load(ctx) lv.load(ctx)
@ -69,8 +69,8 @@ func (lv *LazyValue) Write(ctx context.Context, writer func(v core.Value, err er
// Reset resets the storage. // Reset resets the storage.
// Next call of Read will trigger the factory function again. // Next call of Read will trigger the factory function again.
func (lv *LazyValue) Reset() { func (lv *LazyValue) Reset() {
lv.Lock() lv.mu.Lock()
defer lv.Unlock() defer lv.mu.Unlock()
lv.ready = false lv.ready = false
lv.value = values.None lv.value = values.None

View File

@ -9,31 +9,24 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values/types" "github.com/MontFerret/ferret/pkg/runtime/values/types"
) )
func SetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Value, value core.Value) error { func SetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value, value core.Value) error {
if path == nil || len(path) == 0 { if len(path) == 0 {
return nil return nil
} }
segment := path[0] return SetInDocument(ctx, page.GetMainFrame(), path, value)
if segment.Type() == types.String {
segment := segment.(values.String)
switch segment {
case "url", "URL":
return doc.SetURL(ctx, values.NewString(value.String()))
case "cookies":
default:
return SetInNode(ctx, doc, path, value)
} }
func SetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Value, value core.Value) error {
if len(path) == 0 {
return nil
} }
return SetInNode(ctx, doc, path, value) return SetInNode(ctx, doc, path, value)
} }
func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value, value core.Value) error { func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value, value core.Value) error {
if path == nil || len(path) == 0 { if len(path) == 0 {
return nil return nil
} }
@ -115,7 +108,7 @@ func SetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value
} }
func SetInNode(_ context.Context, _ drivers.HTMLNode, path []core.Value, _ core.Value) error { func SetInNode(_ context.Context, _ drivers.HTMLNode, path []core.Value, _ core.Value) error {
if path == nil || len(path) == 0 { if len(path) == 0 {
return nil return nil
} }

View File

@ -3,12 +3,13 @@ package common
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/MontFerret/ferret/pkg/runtime/core"
"strconv" "strconv"
"strings" "strings"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/gorilla/css/scanner" "github.com/gorilla/css/scanner"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
) )
func DeserializeStyles(input values.String) (*values.Object, error) { func DeserializeStyles(input values.String) (*values.Object, error) {

View File

@ -1,8 +1,10 @@
package common package common
import "golang.org/x/net/html" import (
"golang.org/x/net/html"
)
func ToHTMLType(nt html.NodeType) int { func FromHTMLType(nt html.NodeType) int {
switch nt { switch nt {
case html.DocumentNode: case html.DocumentNode:
return 9 return 9
@ -18,3 +20,20 @@ func ToHTMLType(nt html.NodeType) int {
return 0 return 0
} }
func ToHTMLType(input int) html.NodeType {
switch input {
case 1:
return html.ElementNode
case 3:
return html.TextNode
case 8:
return html.CommentNode
case 9:
return html.DocumentNode
case 10:
return html.DoctypeNode
default:
return html.ErrorNode
}
}

View File

@ -38,6 +38,17 @@ const (
SameSiteStrictMode SameSiteStrictMode
) )
func (s SameSite) String() string {
switch s {
case SameSiteLaxMode:
return "Lax"
case SameSiteStrictMode:
return "Strict"
default:
return ""
}
}
func (c HTTPCookie) Type() core.Type { func (c HTTPCookie) Type() core.Type {
return HTTPCookieType return HTTPCookieType
} }
@ -119,7 +130,7 @@ func (c HTTPCookie) Hash() uint64 {
h.Write([]byte(strconv.Itoa(c.MaxAge))) h.Write([]byte(strconv.Itoa(c.MaxAge)))
h.Write([]byte(fmt.Sprintf("%t", c.Secure))) h.Write([]byte(fmt.Sprintf("%t", c.Secure)))
h.Write([]byte(fmt.Sprintf("%t", c.HTTPOnly))) h.Write([]byte(fmt.Sprintf("%t", c.HTTPOnly)))
h.Write([]byte(strconv.Itoa(int(c.SameSite)))) h.Write([]byte(c.SameSite.String()))
return h.Sum64() return h.Sum64()
} }
@ -138,7 +149,7 @@ func (c HTTPCookie) MarshalJSON() ([]byte, error) {
"max_age": c.MaxAge, "max_age": c.MaxAge,
"secure": c.Secure, "secure": c.Secure,
"http_only": c.HTTPOnly, "http_only": c.HTTPOnly,
"same_site": c.SameSite, "same_site": c.SameSite.String(),
} }
out, err := json.Marshal(v) out, err := json.Marshal(v)
@ -181,14 +192,7 @@ func (c HTTPCookie) GetIn(_ context.Context, path []core.Value) (core.Value, err
case "httpOnly": case "httpOnly":
return values.NewBoolean(c.HTTPOnly), nil return values.NewBoolean(c.HTTPOnly), nil
case "sameSite": case "sameSite":
switch c.SameSite { return values.NewString(c.SameSite.String()), nil
case SameSiteLaxMode:
return values.NewString("Lax"), nil
case SameSiteStrictMode:
return values.NewString("Strict"), nil
default:
return values.EmptyString, nil
}
default: default:
return values.None, nil return values.None, nil
} }

View File

@ -0,0 +1,33 @@
package drivers_test
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/MontFerret/ferret/pkg/drivers"
)
func TestHTTPCookie(t *testing.T) {
Convey("HTTPCookie", t, func() {
Convey(".MarshalJSON", func() {
Convey("Should serialize cookie values", func() {
cookie := &drivers.HTTPCookie{}
cookie.Name = "test_cookie"
cookie.Value = "test_value"
cookie.Domain = "montferret.dev"
cookie.HTTPOnly = true
cookie.MaxAge = 320
cookie.Path = "/"
cookie.SameSite = drivers.SameSiteLaxMode
cookie.Secure = true
out, err := cookie.MarshalJSON()
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `{"domain":"montferret.dev","expires":"0001-01-01T00:00:00Z","http_only":true,"max_age":320,"name":"test_cookie","path":"/","same_site":"Lax","secure":true,"value":"test_value"}`)
})
})
})
}

View File

@ -18,7 +18,7 @@ type (
drivers map[string]Driver drivers map[string]Driver
} }
LoadDocumentParams struct { OpenPageParams struct {
URL string URL string
UserAgent string UserAgent string
KeepCookies bool KeepCookies bool
@ -29,7 +29,7 @@ type (
Driver interface { Driver interface {
io.Closer io.Closer
Name() string Name() string
LoadDocument(ctx context.Context, params LoadDocumentParams) (HTMLDocument, error) Open(ctx context.Context, params OpenPageParams) (HTMLPage, error)
} }
) )

View File

@ -15,7 +15,7 @@ import (
"github.com/MontFerret/ferret/pkg/runtime/values/types" "github.com/MontFerret/ferret/pkg/runtime/values/types"
) )
// HTTPCookie HTTPCookie object // HTTPHeader HTTP header object
type HTTPHeader map[string][]string type HTTPHeader map[string][]string
func (h HTTPHeader) Type() core.Type { func (h HTTPHeader) Type() core.Type {
@ -101,7 +101,7 @@ func (h HTTPHeader) Copy() core.Value {
} }
func (h HTTPHeader) MarshalJSON() ([]byte, error) { func (h HTTPHeader) MarshalJSON() ([]byte, error) {
out, err := json.Marshal(h) out, err := json.Marshal(map[string][]string(h))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -0,0 +1,26 @@
package drivers_test
import (
"github.com/MontFerret/ferret/pkg/drivers"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestHTTPHeader(t *testing.T) {
Convey("HTTPHeader", t, func() {
Convey(".MarshalJSON", func() {
Convey("Should serialize header values", func() {
headers := make(drivers.HTTPHeader)
headers["content-encoding"] = []string{"gzip"}
headers["content-type"] = []string{"text/html", "charset=utf-8"}
out, err := headers.MarshalJSON()
So(err, ShouldBeNil)
So(string(out), ShouldEqual, `{"content-encoding":["gzip"],"content-type":["text/html","charset=utf-8"]}`)
})
})
})
}

View File

@ -2,8 +2,53 @@ package drivers
import ( import (
"context" "context"
"github.com/MontFerret/ferret/pkg/runtime/core"
) )
func WithDefaultTimeout(ctx context.Context) (context.Context, context.CancelFunc) { func WithDefaultTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, DefaultTimeout) return context.WithTimeout(ctx, DefaultTimeout)
} }
func ToPage(value core.Value) (HTMLPage, error) {
err := core.ValidateType(value, HTMLPageType)
if err != nil {
return nil, err
}
return value.(HTMLPage), nil
}
func ToDocument(value core.Value) (HTMLDocument, error) {
switch v := value.(type) {
case HTMLPage:
return v.GetMainFrame(), nil
case HTMLDocument:
return v, nil
default:
return nil, core.TypeError(
value.Type(),
HTMLPageType,
HTMLDocumentType,
)
}
}
func ToElement(value core.Value) (HTMLElement, error) {
switch v := value.(type) {
case HTMLPage:
return v.GetMainFrame().GetElement(), nil
case HTMLDocument:
return v.GetElement(), nil
case HTMLElement:
return v, nil
default:
return nil, core.TypeError(
value.Type(),
HTMLPageType,
HTMLDocumentType,
HTMLElementType,
)
}
}

View File

@ -12,17 +12,25 @@ import (
) )
type HTMLDocument struct { type HTMLDocument struct {
docNode *goquery.Document doc *goquery.Document
element drivers.HTMLElement element drivers.HTMLElement
url values.String url values.String
cookies []drivers.HTTPCookie parent drivers.HTMLDocument
children *values.Array
}
func NewRootHTMLDocument(
node *goquery.Document,
url string,
) (*HTMLDocument, error) {
return NewHTMLDocument(node, url, nil)
} }
func NewHTMLDocument( func NewHTMLDocument(
node *goquery.Document, node *goquery.Document,
url string, url string,
cookies []drivers.HTTPCookie, parent drivers.HTMLDocument,
) (drivers.HTMLDocument, error) { ) (*HTMLDocument, error) {
if url == "" { if url == "" {
return nil, core.Error(core.ErrMissedArgument, "document url") return nil, core.Error(core.ErrMissedArgument, "document url")
} }
@ -37,7 +45,21 @@ func NewHTMLDocument(
return nil, err return nil, err
} }
return &HTMLDocument{node, el, values.NewString(url), cookies}, nil doc := new(HTMLDocument)
doc.doc = node
doc.element = el
doc.parent = parent
doc.url = values.NewString(url)
doc.children = values.NewArray(10)
frames := node.Find("iframe")
frames.Each(func(i int, selection *goquery.Selection) {
child, _ := NewHTMLDocument(goquery.NewDocumentFromNode(selection.Nodes[0]), selection.AttrOr("src", url), doc)
doc.children.Push(child)
})
return doc, nil
} }
func (doc *HTMLDocument) MarshalJSON() ([]byte, error) { func (doc *HTMLDocument) MarshalJSON() ([]byte, error) {
@ -49,7 +71,7 @@ func (doc *HTMLDocument) Type() core.Type {
} }
func (doc *HTMLDocument) String() string { func (doc *HTMLDocument) String() string {
str, err := doc.docNode.Html() str, err := doc.doc.Html()
if err != nil { if err != nil {
return "" return ""
@ -70,7 +92,7 @@ func (doc *HTMLDocument) Compare(other core.Value) int64 {
} }
func (doc *HTMLDocument) Unwrap() interface{} { func (doc *HTMLDocument) Unwrap() interface{} {
return doc.docNode return doc.doc
} }
func (doc *HTMLDocument) Hash() uint64 { func (doc *HTMLDocument) Hash() uint64 {
@ -84,7 +106,7 @@ func (doc *HTMLDocument) Hash() uint64 {
} }
func (doc *HTMLDocument) Copy() core.Value { func (doc *HTMLDocument) Copy() core.Value {
cp, err := NewHTMLDocument(doc.docNode, string(doc.url), doc.cookies) cp, err := NewHTMLDocument(doc.doc, string(doc.url), doc.parent)
if err != nil { if err != nil {
return values.None return values.None
@ -94,27 +116,17 @@ func (doc *HTMLDocument) Copy() core.Value {
} }
func (doc *HTMLDocument) Clone() core.Value { func (doc *HTMLDocument) Clone() core.Value {
var cookies []drivers.HTTPCookie cloned, err := NewHTMLDocument(doc.doc, doc.url.String(), doc.parent)
if doc.cookies != nil {
cookies = make([]drivers.HTTPCookie, 0, len(doc.cookies))
for i, c := range doc.cookies {
cookies[i] = c
}
}
cp, err := NewHTMLDocument(goquery.CloneDocument(doc.docNode), string(doc.url), cookies)
if err != nil { if err != nil {
return values.None return values.None
} }
return cp return cloned
} }
func (doc *HTMLDocument) Length() values.Int { func (doc *HTMLDocument) Length() values.Int {
return values.NewInt(doc.docNode.Length()) return values.NewInt(doc.doc.Length())
} }
func (doc *HTMLDocument) Iterate(_ context.Context) (core.Iterator, error) { func (doc *HTMLDocument) Iterate(_ context.Context) (core.Iterator, error) {
@ -129,11 +141,11 @@ func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value cor
return common.SetInDocument(ctx, doc, path, value) return common.SetInDocument(ctx, doc, path, value)
} }
func (doc *HTMLDocument) NodeType() values.Int { func (doc *HTMLDocument) GetNodeType() values.Int {
return 9 return 9
} }
func (doc *HTMLDocument) NodeName() values.String { func (doc *HTMLDocument) GetNodeName() values.String {
return "#document" return "#document"
} }
@ -161,50 +173,38 @@ func (doc *HTMLDocument) ExistsBySelector(ctx context.Context, selector values.S
return doc.element.ExistsBySelector(ctx, selector) return doc.element.ExistsBySelector(ctx, selector)
} }
func (doc *HTMLDocument) DocumentElement() drivers.HTMLElement { func (doc *HTMLDocument) XPath(ctx context.Context, expression values.String) (core.Value, error) {
return doc.element return doc.element.XPath(ctx, expression)
} }
func (doc *HTMLDocument) GetURL() core.Value { func (doc *HTMLDocument) IsDetached() values.Boolean {
return values.False
}
func (doc *HTMLDocument) GetTitle() values.String {
title := doc.doc.Find("head > title")
return values.NewString(title.Text())
}
func (doc *HTMLDocument) GetChildDocuments(_ context.Context) (*values.Array, error) {
return doc.children.Clone().(*values.Array), nil
}
func (doc *HTMLDocument) GetURL() values.String {
return doc.url return doc.url
} }
func (doc *HTMLDocument) SetURL(_ context.Context, _ values.String) error { func (doc *HTMLDocument) GetElement() drivers.HTMLElement {
return core.ErrInvalidOperation return doc.element
} }
func (doc *HTMLDocument) GetCookies(_ context.Context) (*values.Array, error) { func (doc *HTMLDocument) GetName() values.String {
if doc.cookies == nil { return ""
return values.NewArray(0), nil
} }
arr := values.NewArray(len(doc.cookies)) func (doc *HTMLDocument) GetParentDocument() drivers.HTMLDocument {
return doc.parent
for _, c := range doc.cookies {
arr.Push(c)
}
return arr, nil
}
func (doc *HTMLDocument) SetCookies(_ context.Context, _ ...drivers.HTTPCookie) error {
return core.ErrNotSupported
}
func (doc *HTMLDocument) DeleteCookies(_ context.Context, _ ...drivers.HTTPCookie) error {
return core.ErrNotSupported
}
func (doc *HTMLDocument) Navigate(_ context.Context, _ values.String) error {
return core.ErrNotSupported
}
func (doc *HTMLDocument) NavigateBack(_ context.Context, _ values.Int) (values.Boolean, error) {
return false, core.ErrNotSupported
}
func (doc *HTMLDocument) NavigateForward(_ context.Context, _ values.Int) (values.Boolean, error) {
return false, core.ErrNotSupported
} }
func (doc *HTMLDocument) ClickBySelector(_ context.Context, _ values.String) (values.Boolean, error) { func (doc *HTMLDocument) ClickBySelector(_ context.Context, _ values.String) (values.Boolean, error) {

View File

@ -2,10 +2,12 @@ package http_test
import ( import (
"bytes" "bytes"
"testing"
"github.com/MontFerret/ferret/pkg/drivers/http" "github.com/MontFerret/ferret/pkg/drivers/http"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"testing"
) )
func TestDocument(t *testing.T) { func TestDocument(t *testing.T) {
@ -218,7 +220,7 @@ func TestDocument(t *testing.T) {
</footer> </footer>
<svg xmlns="http://www.w3.org/2000/svg" width="348" height="225" viewBox="0 0 348 225" preserveAspectRatio="none" style="display: none; visibility: hidden; position: absolute; top: -100%; left: -100%;"><defs><style type="text/css"></style></defs><text x="0" y="17" style="font-weight:bold;font-size:17pt;font-family:Arial, Helvetica, Open Sans, sans-serif">Thumbnail</text></svg></body></html> <svg xmlns="http://www.w3.org/2000/svg" width="348" height="225" viewBox="0 0 348 225" preserveAspectRatio="none" style="display: none; visibility: hidden; position: absolute; top: -100%; left: -100%;"><defs><style type="text/css"></style></defs><text x="0" y="17" style="font-weight:bold;font-size:17pt;font-family:Arial, Helvetica, Open Sans, sans-serif">Thumbnail</text></svg></body></html>
` `
Convey(".NodeType", t, func() { Convey(".GetNodeType", t, func() {
Convey("Should serialize a boolean value", func() { Convey("Should serialize a boolean value", func() {
buff := bytes.NewBuffer([]byte(doc)) buff := bytes.NewBuffer([]byte(doc))
@ -232,7 +234,7 @@ func TestDocument(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(el.NodeType(), ShouldEqual, 9) So(el.GetNodeType(), ShouldEqual, 9)
}) })
}) })
} }

View File

@ -62,7 +62,7 @@ func (drv *Driver) Name() string {
return DriverName return DriverName
} }
func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocumentParams) (drivers.HTMLDocument, error) { func (drv *Driver) Open(ctx context.Context, params drivers.OpenPageParams) (drivers.HTMLPage, error) {
req, err := http.NewRequest(http.MethodGet, params.URL, nil) req, err := http.NewRequest(http.MethodGet, params.URL, nil)
if err != nil { if err != nil {
@ -119,6 +119,10 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument
Str("user-agent", ua). Str("user-agent", ua).
Msg("using User-Agent") Msg("using User-Agent")
if ua != "" {
req.Header.Set("User-Agent", ua)
}
resp, err := drv.client.Do(req) resp, err := drv.client.Do(req)
if err != nil { if err != nil {
@ -127,16 +131,20 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(resp.Status)
}
doc, err := goquery.NewDocumentFromReader(resp.Body) doc, err := goquery.NewDocumentFromReader(resp.Body)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to parse a document %s", params.URL) return nil, errors.Wrapf(err, "failed to parse a document %s", params.URL)
} }
return NewHTMLDocument(doc, params.URL, params.Cookies) return NewHTMLPage(doc, params.URL, params.Cookies)
} }
func (drv *Driver) ParseDocument(_ context.Context, str values.String) (drivers.HTMLDocument, error) { func (drv *Driver) Parse(_ context.Context, str values.String) (drivers.HTMLPage, error) {
buf := bytes.NewBuffer([]byte(str)) buf := bytes.NewBuffer([]byte(str))
doc, err := goquery.NewDocumentFromReader(buf) doc, err := goquery.NewDocumentFromReader(buf)
@ -145,7 +153,7 @@ func (drv *Driver) ParseDocument(_ context.Context, str values.String) (drivers.
return nil, errors.Wrap(err, "failed to parse a document") return nil, errors.Wrap(err, "failed to parse a document")
} }
return NewHTMLDocument(doc, "#string", nil) return NewHTMLPage(doc, "#blank", nil)
} }
func (drv *Driver) Close() error { func (drv *Driver) Close() error {

View File

@ -3,6 +3,8 @@ package http
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"github.com/antchfx/htmlquery"
"hash/fnv" "hash/fnv"
"strings" "strings"
@ -10,7 +12,9 @@ import (
"github.com/MontFerret/ferret/pkg/drivers/common" "github.com/MontFerret/ferret/pkg/drivers/common"
"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/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/antchfx/xpath"
) )
type HTMLElement struct { type HTMLElement struct {
@ -29,7 +33,7 @@ func NewHTMLElement(node *goquery.Selection) (drivers.HTMLElement, error) {
} }
func (el *HTMLElement) MarshalJSON() ([]byte, error) { func (el *HTMLElement) MarshalJSON() ([]byte, error) {
return json.Marshal(el.InnerText(context.Background()).String()) return json.Marshal(el.GetInnerText(context.Background()).String())
} }
func (el *HTMLElement) Type() core.Type { func (el *HTMLElement) Type() core.Type {
@ -37,7 +41,7 @@ func (el *HTMLElement) Type() core.Type {
} }
func (el *HTMLElement) String() string { func (el *HTMLElement) String() string {
return el.InnerHTML(context.Background()).String() return el.GetInnerHTML(context.Background()).String()
} }
func (el *HTMLElement) Compare(other core.Value) int64 { func (el *HTMLElement) Compare(other core.Value) int64 {
@ -48,7 +52,7 @@ func (el *HTMLElement) Compare(other core.Value) int64 {
ctx, fn := drivers.WithDefaultTimeout(context.Background()) ctx, fn := drivers.WithDefaultTimeout(context.Background())
defer fn() defer fn()
return el.InnerHTML(ctx).Compare(other.InnerHTML(ctx)) return el.GetInnerHTML(ctx).Compare(other.GetInnerHTML(ctx))
default: default:
return drivers.Compare(el.Type(), other.Type()) return drivers.Compare(el.Type(), other.Type())
} }
@ -80,21 +84,25 @@ func (el *HTMLElement) Copy() core.Value {
return c return c
} }
func (el *HTMLElement) NodeType() values.Int { func (el *HTMLElement) IsDetached() values.Boolean {
return values.True
}
func (el *HTMLElement) GetNodeType() values.Int {
nodes := el.selection.Nodes nodes := el.selection.Nodes
if len(nodes) == 0 { if len(nodes) == 0 {
return 0 return 0
} }
return values.NewInt(common.ToHTMLType(nodes[0].Type)) return values.NewInt(common.FromHTMLType(nodes[0].Type))
} }
func (el *HTMLElement) Close() error { func (el *HTMLElement) Close() error {
return nil return nil
} }
func (el *HTMLElement) NodeName() values.String { func (el *HTMLElement) GetNodeName() values.String {
return values.NewString(goquery.NodeName(el.selection)) return values.NewString(goquery.NodeName(el.selection))
} }
@ -122,11 +130,11 @@ func (el *HTMLElement) SetValue(_ context.Context, value core.Value) error {
return nil return nil
} }
func (el *HTMLElement) InnerText(_ context.Context) values.String { func (el *HTMLElement) GetInnerText(_ context.Context) values.String {
return values.NewString(el.selection.Text()) return values.NewString(el.selection.Text())
} }
func (el *HTMLElement) InnerHTML(_ context.Context) values.String { func (el *HTMLElement) GetInnerHTML(_ context.Context) values.String {
h, err := el.selection.Html() h, err := el.selection.Html()
if err != nil { if err != nil {
@ -304,6 +312,53 @@ func (el *HTMLElement) QuerySelectorAll(_ context.Context, selector values.Strin
return arr return arr
} }
func (el *HTMLElement) XPath(_ context.Context, expression values.String) (core.Value, error) {
h, err := outerHTML(el.selection)
if err != nil {
return values.None, err
}
exp, err := xpath.Compile(expression.String())
if err != nil {
return values.None, err
}
rootNode, err := htmlquery.Parse(strings.NewReader(h))
if err != nil {
return values.None, err
}
fmt.Println(htmlquery.OutputHTML(rootNode, true))
out := exp.Evaluate(htmlquery.CreateXPathNavigator(rootNode))
switch res := out.(type) {
case *xpath.NodeIterator:
items := values.NewArray(10)
for {
if !res.MoveNext() {
break
}
item, err := parseXPathNode(res.Current().(*htmlquery.NodeNavigator))
if err != nil {
return values.None, err
}
items.Push(item)
}
return items, nil
default:
return values.Parse(res), nil
}
}
func (el *HTMLElement) InnerHTMLBySelector(_ context.Context, selector values.String) values.String { func (el *HTMLElement) InnerHTMLBySelector(_ context.Context, selector values.String) values.String {
selection := el.selection.Find(selector.String()) selection := el.selection.Find(selector.String())

View File

@ -244,7 +244,7 @@ func TestElement(t *testing.T) {
<svg xmlns="http://www.w3.org/2000/svg" width="348" height="225" viewBox="0 0 348 225" preserveAspectRatio="none" style="display: none; visibility: hidden; position: absolute; top: -100%; left: -100%;"><defs><style type="text/css"></style></defs><text x="0" y="17" style="font-weight:bold;font-size:17pt;font-family:Arial, Helvetica, Open Sans, sans-serif">Thumbnail</text></svg></body></html> <svg xmlns="http://www.w3.org/2000/svg" width="348" height="225" viewBox="0 0 348 225" preserveAspectRatio="none" style="display: none; visibility: hidden; position: absolute; top: -100%; left: -100%;"><defs><style type="text/css"></style></defs><text x="0" y="17" style="font-weight:bold;font-size:17pt;font-family:Arial, Helvetica, Open Sans, sans-serif">Thumbnail</text></svg></body></html>
` `
Convey(".NodeType", t, func() { Convey(".GetNodeType", t, func() {
buff := bytes.NewBuffer([]byte(doc)) buff := bytes.NewBuffer([]byte(doc))
buff.Write([]byte(doc)) buff.Write([]byte(doc))
@ -257,10 +257,10 @@ func TestElement(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(el.NodeType(), ShouldEqual, 1) So(el.GetNodeType(), ShouldEqual, 1)
}) })
Convey(".NodeName", t, func() { Convey(".GetNodeName", t, func() {
buff := bytes.NewBuffer([]byte(doc)) buff := bytes.NewBuffer([]byte(doc))
buff.Write([]byte(doc)) buff.Write([]byte(doc))
@ -273,7 +273,7 @@ func TestElement(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(el.NodeName(), ShouldEqual, "body") So(el.GetNodeName(), ShouldEqual, "body")
}) })
Convey(".Length", t, func() { Convey(".Length", t, func() {
@ -327,7 +327,7 @@ func TestElement(t *testing.T) {
So(v, ShouldEqual, "find") So(v, ShouldEqual, "find")
}) })
Convey(".InnerText", t, func() { Convey(".GetInnerText", t, func() {
buff := bytes.NewBuffer([]byte(` buff := bytes.NewBuffer([]byte(`
<html> <html>
<head></head> <head></head>
@ -349,7 +349,7 @@ func TestElement(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
v := el.InnerText(context.Background()) v := el.GetInnerText(context.Background())
So(v, ShouldEqual, "Ferret") So(v, ShouldEqual, "Ferret")
}) })
@ -376,7 +376,7 @@ func TestElement(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
v := el.InnerHTML(context.Background()) v := el.GetInnerHTML(context.Background())
So(v, ShouldEqual, "<h2>Ferret</h2>") So(v, ShouldEqual, "<h2>Ferret</h2>")
}) })
@ -396,7 +396,7 @@ func TestElement(t *testing.T) {
So(found, ShouldNotEqual, values.None) So(found, ShouldNotEqual, values.None)
v := found.(drivers.HTMLNode).NodeName() v := found.(drivers.HTMLNode).GetNodeName()
So(err, ShouldBeNil) So(err, ShouldBeNil)

View File

@ -0,0 +1,47 @@
package http
import (
"bytes"
"golang.org/x/net/html"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/PuerkitoBio/goquery"
"github.com/antchfx/htmlquery"
"github.com/antchfx/xpath"
)
func parseXPathNode(nav *htmlquery.NodeNavigator) (core.Value, error) {
node := nav.Current()
if node == nil {
return values.None, nil
}
switch nav.NodeType() {
case xpath.ElementNode:
return NewHTMLElement(&goquery.Selection{Nodes: []*html.Node{node}})
case xpath.RootNode:
url := htmlquery.SelectAttr(node, "url")
return NewHTMLDocument(goquery.NewDocumentFromNode(node), url, nil)
default:
return values.Parse(node.Data), nil
}
}
func outerHTML(s *goquery.Selection) (string, error) {
var buf bytes.Buffer
if len(s.Nodes) > 0 {
c := s.Nodes[0]
err := html.Render(&buf, c)
if err != nil {
return "", err
}
}
return buf.String(), nil
}

199
pkg/drivers/http/page.go Normal file
View File

@ -0,0 +1,199 @@
package http
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/drivers/common"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/PuerkitoBio/goquery"
"hash/fnv"
)
type HTMLPage struct {
document *HTMLDocument
cookies []drivers.HTTPCookie
frames *values.Array
}
func NewHTMLPage(
qdoc *goquery.Document,
url string,
cookies []drivers.HTTPCookie,
) (*HTMLPage, error) {
doc, err := NewRootHTMLDocument(qdoc, url)
if err != nil {
return nil, err
}
p := new(HTMLPage)
p.document = doc
p.cookies = cookies
p.frames = nil
return p, nil
}
func (p *HTMLPage) MarshalJSON() ([]byte, error) {
return p.document.MarshalJSON()
}
func (p *HTMLPage) Type() core.Type {
return drivers.HTMLPageType
}
func (p *HTMLPage) String() string {
return p.document.GetURL().String()
}
func (p *HTMLPage) Compare(other core.Value) int64 {
tc := drivers.Compare(p.Type(), other.Type())
if tc != 0 {
return tc
}
httpPage, ok := other.(*HTMLPage)
if !ok {
return 1
}
return p.document.GetURL().Compare(httpPage.GetURL())
}
func (p *HTMLPage) Unwrap() interface{} {
return p.document
}
func (p *HTMLPage) Hash() uint64 {
h := fnv.New64a()
h.Write([]byte("HTTP"))
h.Write([]byte(p.Type().String()))
h.Write([]byte(":"))
h.Write([]byte(p.document.GetURL()))
return h.Sum64()
}
func (p *HTMLPage) Copy() core.Value {
page, err := NewHTMLPage(p.document.doc, p.document.GetURL().String(), p.cookies[:])
if err != nil {
return values.None
}
return page
}
func (p *HTMLPage) Iterate(ctx context.Context) (core.Iterator, error) {
return p.document.Iterate(ctx)
}
func (p *HTMLPage) GetIn(ctx context.Context, path []core.Value) (core.Value, error) {
return common.GetInPage(ctx, p, path)
}
func (p *HTMLPage) SetIn(ctx context.Context, path []core.Value, value core.Value) error {
return common.SetInPage(ctx, p, path, value)
}
func (p *HTMLPage) Length() values.Int {
return p.document.Length()
}
func (p *HTMLPage) Close() error {
return nil
}
func (p *HTMLPage) IsClosed() values.Boolean {
return values.True
}
func (p *HTMLPage) GetURL() values.String {
return p.document.GetURL()
}
func (p *HTMLPage) GetMainFrame() drivers.HTMLDocument {
return p.document
}
func (p *HTMLPage) GetFrames(ctx context.Context) (*values.Array, error) {
if p.frames == nil {
arr := values.NewArray(10)
err := common.CollectFrames(ctx, arr, p.document)
if err != nil {
return values.NewArray(0), err
}
p.frames = arr
}
return p.frames, nil
}
func (p *HTMLPage) GetFrame(ctx context.Context, idx values.Int) (core.Value, error) {
if p.frames == nil {
arr := values.NewArray(10)
err := common.CollectFrames(ctx, arr, p.document)
if err != nil {
return values.None, err
}
p.frames = arr
}
return p.frames.Get(idx), nil
}
func (p *HTMLPage) GetCookies(_ context.Context) (*values.Array, error) {
if p.cookies == nil {
return values.NewArray(0), nil
}
arr := values.NewArray(len(p.cookies))
for _, c := range p.cookies {
arr.Push(c)
}
return arr, nil
}
func (p *HTMLPage) SetCookies(_ context.Context, _ ...drivers.HTTPCookie) error {
return core.ErrNotSupported
}
func (p *HTMLPage) DeleteCookies(_ context.Context, _ ...drivers.HTTPCookie) error {
return core.ErrNotSupported
}
func (p *HTMLPage) PrintToPDF(_ context.Context, _ drivers.PDFParams) (values.Binary, error) {
return nil, core.ErrNotSupported
}
func (p *HTMLPage) CaptureScreenshot(_ context.Context, _ drivers.ScreenshotParams) (values.Binary, error) {
return nil, core.ErrNotSupported
}
func (p *HTMLPage) WaitForNavigation(_ context.Context) error {
return core.ErrNotSupported
}
func (p *HTMLPage) Navigate(_ context.Context, _ values.String) error {
return core.ErrNotSupported
}
func (p *HTMLPage) NavigateBack(_ context.Context, _ values.Int) (values.Boolean, error) {
return false, core.ErrNotSupported
}
func (p *HTMLPage) NavigateForward(_ context.Context, _ values.Int) (values.Boolean, error) {
return false, core.ErrNotSupported
}

View File

@ -7,6 +7,7 @@ var (
HTTPCookieType = core.NewType("HTTPCookie") HTTPCookieType = core.NewType("HTTPCookie")
HTMLElementType = core.NewType("HTMLElement") HTMLElementType = core.NewType("HTMLElement")
HTMLDocumentType = core.NewType("HTMLDocument") HTMLDocumentType = core.NewType("HTMLDocument")
HTMLPageType = core.NewType("HTMLPageType")
) )
// Comparison table of builtin types // Comparison table of builtin types
@ -15,6 +16,7 @@ var typeComparisonTable = map[core.Type]uint64{
HTTPCookieType: 1, HTTPCookieType: 1,
HTMLElementType: 2, HTMLElementType: 2,
HTMLDocumentType: 3, HTMLDocumentType: 3,
HTMLPageType: 4,
} }
func Compare(first, second core.Type) int64 { func Compare(first, second core.Type) int64 {

View File

@ -24,9 +24,11 @@ type (
collections.Measurable collections.Measurable
io.Closer io.Closer
NodeType() values.Int IsDetached() values.Boolean
NodeName() values.String GetNodeType() values.Int
GetNodeName() values.String
GetChildNodes(ctx context.Context) core.Value GetChildNodes(ctx context.Context) core.Value
@ -39,15 +41,17 @@ type (
CountBySelector(ctx context.Context, selector values.String) values.Int CountBySelector(ctx context.Context, selector values.String) values.Int
ExistsBySelector(ctx context.Context, selector values.String) values.Boolean ExistsBySelector(ctx context.Context, selector values.String) values.Boolean
XPath(ctx context.Context, expression values.String) (core.Value, error)
} }
// HTMLElement is the most general base interface which most objects in a Document implement. // HTMLElement is the most general base interface which most objects in a GetMainFrame implement.
HTMLElement interface { HTMLElement interface {
HTMLNode HTMLNode
InnerText(ctx context.Context) values.String GetInnerText(ctx context.Context) values.String
InnerHTML(ctx context.Context) values.String GetInnerHTML(ctx context.Context) values.String
GetValue(ctx context.Context) core.Value GetValue(ctx context.Context) core.Value
@ -98,28 +102,20 @@ type (
WaitForClass(ctx context.Context, class values.String, when WaitEvent) error WaitForClass(ctx context.Context, class values.String, when WaitEvent) error
} }
// The Document interface represents any web page loaded in the browser
// and serves as an entry point into the web page's content, which is the DOM tree.
HTMLDocument interface { HTMLDocument interface {
HTMLNode HTMLNode
DocumentElement() HTMLElement GetTitle() values.String
GetURL() core.Value GetElement() HTMLElement
SetURL(ctx context.Context, url values.String) error GetURL() values.String
GetCookies(ctx context.Context) (*values.Array, error) GetName() values.String
SetCookies(ctx context.Context, cookies ...HTTPCookie) error GetParentDocument() HTMLDocument
DeleteCookies(ctx context.Context, cookies ...HTTPCookie) error GetChildDocuments(ctx context.Context) (*values.Array, error)
Navigate(ctx context.Context, url values.String) error
NavigateBack(ctx context.Context, skip values.Int) (values.Boolean, error)
NavigateForward(ctx context.Context, skip values.Int) (values.Boolean, error)
ClickBySelector(ctx context.Context, selector values.String) (values.Boolean, error) ClickBySelector(ctx context.Context, selector values.String) (values.Boolean, error)
@ -129,10 +125,6 @@ type (
SelectBySelector(ctx context.Context, selector values.String, value *values.Array) (*values.Array, error) SelectBySelector(ctx context.Context, selector values.String, value *values.Array) (*values.Array, error)
PrintToPDF(ctx context.Context, params PDFParams) (values.Binary, error)
CaptureScreenshot(ctx context.Context, params ScreenshotParams) (values.Binary, error)
ScrollTop(ctx context.Context) error ScrollTop(ctx context.Context) error
ScrollBottom(ctx context.Context) error ScrollBottom(ctx context.Context) error
@ -145,8 +137,6 @@ type (
MoveMouseBySelector(ctx context.Context, selector values.String) error MoveMouseBySelector(ctx context.Context, selector values.String) error
WaitForNavigation(ctx context.Context) error
WaitForElement(ctx context.Context, selector values.String, when WaitEvent) error WaitForElement(ctx context.Context, selector values.String, when WaitEvent) error
WaitForAttributeBySelector(ctx context.Context, selector, name values.String, value core.Value, when WaitEvent) error WaitForAttributeBySelector(ctx context.Context, selector, name values.String, value core.Value, when WaitEvent) error
@ -161,6 +151,45 @@ type (
WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when WaitEvent) error WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when WaitEvent) error
} }
// HTMLPage interface represents any web page loaded in the browser
// and serves as an entry point into the web page's content
HTMLPage interface {
core.Value
core.Iterable
core.Getter
core.Setter
collections.Measurable
io.Closer
IsClosed() values.Boolean
GetURL() values.String
GetMainFrame() HTMLDocument
GetFrames(ctx context.Context) (*values.Array, error)
GetFrame(ctx context.Context, idx values.Int) (core.Value, error)
GetCookies(ctx context.Context) (*values.Array, error)
SetCookies(ctx context.Context, cookies ...HTTPCookie) error
DeleteCookies(ctx context.Context, cookies ...HTTPCookie) error
PrintToPDF(ctx context.Context, params PDFParams) (values.Binary, error)
CaptureScreenshot(ctx context.Context, params ScreenshotParams) (values.Binary, error)
WaitForNavigation(ctx context.Context) error
Navigate(ctx context.Context, url values.String) error
NavigateBack(ctx context.Context, skip values.Int) (values.Boolean, error)
NavigateForward(ctx context.Context, skip values.Int) (values.Boolean, error)
}
) )
const ( const (

View File

@ -86,6 +86,8 @@ FloatLiteral
| DecimalIntegerLiteral ExponentPart? | DecimalIntegerLiteral ExponentPart?
; ;
NamespaceSegment: Identifier NamespaceSeparator;
// Fragments // Fragments
fragment HexDigit fragment HexDigit
: [0-9a-fA-F] : [0-9a-fA-F]
@ -106,3 +108,4 @@ fragment Digit
; ;
fragment DQSring: '"' ( '\\'. | '""' | ~('"'| '\\') )* '"'; fragment DQSring: '"' ( '\\'. | '""' | ~('"'| '\\') )* '"';
fragment SQString: '\'' ('\\'. | '\'\'' | ~('\'' | '\\'))* '\''; fragment SQString: '\'' ('\\'. | '\'\'' | ~('\'' | '\\'))* '\'';
fragment NamespaceSeparator: '::';

View File

@ -60,6 +60,7 @@ StringLiteral=59
TemplateStringLiteral=60 TemplateStringLiteral=60
IntegerLiteral=61 IntegerLiteral=61
FloatLiteral=62 FloatLiteral=62
NamespaceSegment=63
':'=5 ':'=5
';'=6 ';'=6
'.'=7 '.'=7

View File

@ -207,8 +207,12 @@ expressionGroup
: OpenParen expression CloseParen : OpenParen expression CloseParen
; ;
namespace
: (NamespaceSegment)*
;
functionCallExpression functionCallExpression
: Identifier arguments : namespace Identifier arguments
; ;
arguments arguments

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More