mirror of
				https://github.com/MontFerret/ferret.git
				synced 2025-10-30 23:37:40 +02:00 
			
		
		
		
	Feature/#220 iframe support (#315)
* Refactored Virtual DOM structure * Added new E2E tests * Updated E2E Test Runner
This commit is contained in:
		
							
								
								
									
										33
									
								
								.golangci.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								.golangci.yml
									
									
									
									
									
										Normal 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" | ||||
| @@ -18,6 +18,7 @@ addons: | ||||
|  | ||||
| install: | ||||
| - 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 | ||||
| - export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" | ||||
| - mkdir $HOME/travis-bin | ||||
|   | ||||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							| @@ -30,7 +30,7 @@ cover: | ||||
| 	curl -s https://codecov.io/bash | bash | ||||
|  | ||||
| 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: | ||||
| 	go test -run=XXX -bench=. ${DIR_PKG}/... | ||||
| @@ -48,7 +48,8 @@ fmt: | ||||
| # https://github.com/mgechev/revive | ||||
| # go get github.com/mgechev/revive | ||||
| 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 | ||||
| # go get code.google.com/p/go.tools/cmd/vet | ||||
|   | ||||
							
								
								
									
										58
									
								
								e2e/main.go
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								e2e/main.go
									
									
									
									
									
								
							| @@ -4,13 +4,15 @@ import ( | ||||
| 	"context" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"github.com/MontFerret/ferret/e2e/runner" | ||||
| 	"github.com/MontFerret/ferret/e2e/server" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/e2e/runner" | ||||
| 	"github.com/MontFerret/ferret/e2e/server" | ||||
|  | ||||
| 	"github.com/rs/zerolog" | ||||
| ) | ||||
|  | ||||
| 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() { | ||||
| 	flag.Parse() | ||||
|  | ||||
| @@ -56,19 +72,6 @@ func main() { | ||||
| 		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() { | ||||
| 		if err := static.Start(); err != nil { | ||||
| 			logger.Info().Timestamp().Msg("shutting down the static pages server") | ||||
| @@ -91,12 +94,25 @@ func main() { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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{ | ||||
| 		StaticServerAddress:  fmt.Sprintf("http://0.0.0.0:%d", staticPort), | ||||
| 		DynamicServerAddress: fmt.Sprintf("http://0.0.0.0:%d", dynamicPort), | ||||
| 		StaticServerAddress:  fmt.Sprintf("http://%s:%d", ipAddr, staticPort), | ||||
| 		DynamicServerAddress: fmt.Sprintf("http://%s:%d", ipAddr, dynamicPort), | ||||
| 		CDPAddress:           *cdp, | ||||
| 		Dir:                  *testsDir, | ||||
| 		Filter:               filterR, | ||||
| 		Filter:               *filter, | ||||
| 	}) | ||||
|  | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| @@ -110,7 +126,7 @@ func main() { | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	err := r.Run(ctx) | ||||
| 	err = r.Run(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		os.Exit(1) | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import Layout from './layout.js'; | ||||
| import IndexPage from './pages/index.js'; | ||||
| import FormsPage from './pages/forms/index.js'; | ||||
| import EventsPage from './pages/events/index.js'; | ||||
| import IframePage from './pages/iframes/index.js'; | ||||
|  | ||||
| const e = React.createElement; | ||||
| const Router = ReactRouter.Router; | ||||
| @@ -10,7 +11,26 @@ const Route = ReactRouter.Route; | ||||
| const Redirect = ReactRouter.Redirect; | ||||
| 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() }, | ||||
|         e(Layout, null, [ | ||||
|             e(Switch, null, [ | ||||
| @@ -27,8 +47,12 @@ export default function AppComponent({ redirect = null}) { | ||||
|                     path: '/events', | ||||
|                     component: EventsPage | ||||
|                 }), | ||||
|                 e(Route, { | ||||
|                     path: '/iframe', | ||||
|                     component: IframePage | ||||
|                 }), | ||||
|             ]), | ||||
|             redirect ? e(Redirect, { to: redirect }) : null | ||||
|             redirectTo | ||||
|         ]) | ||||
|     ) | ||||
| } | ||||
| }) | ||||
| @@ -18,6 +18,9 @@ export default function Layout({ children }) { | ||||
|                     ]), | ||||
|                     e("li", { className: "nav-item"}, [ | ||||
|                         e(NavLink, { className: "nav-link", to: "/events" }, "Events") | ||||
|                     ]), | ||||
|                     e("li", { className: "nav-item"}, [ | ||||
|                         e(NavLink, { className: "nav-link", to: "/iframe" }, "iFrame") | ||||
|                     ]) | ||||
|                 ]) | ||||
|             ]) | ||||
|   | ||||
							
								
								
									
										26
									
								
								e2e/pages/dynamic/components/pages/iframes/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								e2e/pages/dynamic/components/pages/iframes/index.js
									
									
									
									
									
										Normal 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}` : '/' | ||||
|             }), | ||||
|         ]) | ||||
|     } | ||||
| } | ||||
| @@ -11,9 +11,9 @@ | ||||
|     </head> | ||||
|     <body class="text-center"> | ||||
|         <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-dom@16.6.1/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/react@16.8.6/umd/react.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.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-dom@4.3.1/umd/react-router-dom.js"></script> | ||||
|         <script src="index.js" type="module"></script> | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import ( | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/compiler" | ||||
| @@ -14,6 +13,7 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/cdp" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/http" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime" | ||||
|  | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/rs/zerolog" | ||||
| ) | ||||
| @@ -24,7 +24,7 @@ type ( | ||||
| 		DynamicServerAddress string | ||||
| 		CDPAddress           string | ||||
| 		Dir                  string | ||||
| 		Filter               *regexp.Regexp | ||||
| 		Filter               string | ||||
| 	} | ||||
|  | ||||
| 	Result struct { | ||||
| @@ -84,7 +84,7 @@ func (r *Runner) Run(ctx context.Context) error { | ||||
| 		Timestamp(). | ||||
| 		Int("passed", sum.passed). | ||||
| 		Int("failed", sum.failed). | ||||
| 		Dur("time", sum.duration). | ||||
| 		Str("duration", sum.duration.String()). | ||||
| 		Msg("Completed") | ||||
|  | ||||
| 	if sum.failed > 0 { | ||||
| @@ -95,19 +95,7 @@ func (r *Runner) Run(ctx context.Context) error { | ||||
| } | ||||
|  | ||||
| func (r *Runner) runQueries(ctx context.Context, dir string) ([]Result, 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 nil, err | ||||
| 	} | ||||
|  | ||||
| 	results := make([]Result, 0, len(files)) | ||||
| 	results := make([]Result, 0, 50) | ||||
|  | ||||
| 	c := compiler.New() | ||||
|  | ||||
| @@ -115,46 +103,61 @@ func (r *Runner) runQueries(ctx context.Context, dir string) ([]Result, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// read scripts | ||||
| 	for _, f := range files { | ||||
| 		n := f.Name() | ||||
| 	err := r.traverseDir(ctx, dir, func(name string) error { | ||||
| 		if r.settings.Filter != "" { | ||||
| 			matched, err := filepath.Match(r.settings.Filter, name) | ||||
|  | ||||
| 		if r.settings.Filter != nil { | ||||
| 			if !r.settings.Filter.Match([]byte(n)) { | ||||
| 				continue | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if !matched { | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		fName := filepath.Join(dir, n) | ||||
| 		b, err := ioutil.ReadFile(fName) | ||||
| 		b, err := ioutil.ReadFile(name) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			results = append(results, Result{ | ||||
| 				name: fName, | ||||
| 				name: name, | ||||
| 				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 { | ||||
| 			r.logger.Info(). | ||||
| 				Timestamp(). | ||||
| 				Str("file", result.name). | ||||
| 				Msg("Test passed") | ||||
| 		} else { | ||||
| 			r.logger.Error(). | ||||
| 				Timestamp(). | ||||
| 				Err(result.err). | ||||
| 				Str("file", result.name). | ||||
| 				Msg("Test failed") | ||||
| 			if result.err == nil { | ||||
| 				r.logger.Info(). | ||||
| 					Timestamp(). | ||||
| 					Str("file", result.name). | ||||
| 					Str("duration", result.duration.String()). | ||||
| 					Msg("Test passed") | ||||
| 			} else { | ||||
| 				r.logger.Error(). | ||||
| 					Timestamp(). | ||||
| 					Err(result.err). | ||||
| 					Str("file", result.name). | ||||
| 					Str("duration", result.duration.String()). | ||||
| 					Msg("Test failed") | ||||
| 			} | ||||
|  | ||||
| 			results = append(results, result) | ||||
| 		} | ||||
|  | ||||
| 		results = append(results, result) | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return results, nil | ||||
| @@ -237,3 +240,35 @@ func (r *Runner) report(results []Result) Summary { | ||||
| 		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 | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| LET url = @dynamic | ||||
| LET doc = DOCUMENT(url, true) | ||||
|  | ||||
| LET expected = `<!DOCTYPE html><html lang="en"><head> | ||||
| LET expected = `<head> | ||||
|                                                        <meta charset="utf-8"> | ||||
|                                                        <meta http-equiv="x-ua-compatible" content="ie=edge"> | ||||
|                                                        <title>Ferret E2E SPA</title> | ||||
| @@ -11,16 +11,16 @@ LET expected = `<!DOCTYPE html><html lang="en"><head> | ||||
|                                                        <link rel="stylesheet" href="index.css"> | ||||
|                                                    </head> | ||||
|                                                    <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> | ||||
|                                                        <script src="https://unpkg.com/react@16.6.1/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/history@4.7.2/umd/history.min.js"></script> | ||||
|                                                        <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.8.6/umd/react.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.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-dom@4.3.1/umd/react-router-dom.js"></script> | ||||
|                                                        <script src="index.js" type="module"></script> | ||||
|  | ||||
|  | ||||
|                                                </body></html>` | ||||
|                                                </body>` | ||||
| LET actual = INNER_HTML(doc) | ||||
|  | ||||
| LET r1 = '(\s|\")' | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| LET url = @dynamic | ||||
| LET doc = DOCUMENT(url, true) | ||||
|  | ||||
| LET expected = `Ferret E2E SPA | ||||
| Ferret | ||||
| LET expected = `Ferret | ||||
| Forms | ||||
| Navigation | ||||
| Events | ||||
| iFrame | ||||
| Welcome to Ferret E2E test page! | ||||
| It has several pages for testing different possibilities of the library | ||||
| ` | ||||
|   | ||||
							
								
								
									
										12
									
								
								e2e/tests/dynamic/doc/iframes/element_exists.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								e2e/tests/dynamic/doc/iframes/element_exists.fql
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										12
									
								
								e2e/tests/dynamic/doc/iframes/hover.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								e2e/tests/dynamic/doc/iframes/hover.fql
									
									
									
									
									
										Normal 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.") | ||||
							
								
								
									
										11
									
								
								e2e/tests/dynamic/doc/iframes/input.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								e2e/tests/dynamic/doc/iframes/input.fql
									
									
									
									
									
										Normal 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") | ||||
							
								
								
									
										4
									
								
								e2e/tests/dynamic/doc/iframes/length.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								e2e/tests/dynamic/doc/iframes/length.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| LET url = @dynamic + "?redirect=/iframe" | ||||
| LET doc = DOCUMENT(url, { driver: 'cdp' }) | ||||
|  | ||||
| RETURN EXPECT(2, LENGTH(doc.frames)) | ||||
							
								
								
									
										15
									
								
								e2e/tests/dynamic/doc/iframes/wait_class.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								e2e/tests/dynamic/doc/iframes/wait_class.fql
									
									
									
									
									
										Normal 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 "" | ||||
							
								
								
									
										14
									
								
								e2e/tests/dynamic/element/iframes/hover.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								e2e/tests/dynamic/element/iframes/hover.fql
									
									
									
									
									
										Normal 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.") | ||||
							
								
								
									
										12
									
								
								e2e/tests/dynamic/element/iframes/input.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								e2e/tests/dynamic/element/iframes/input.fql
									
									
									
									
									
										Normal 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") | ||||
							
								
								
									
										11
									
								
								e2e/tests/dynamic/element/iframes/select_single.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								e2e/tests/dynamic/element/iframes/select_single.fql
									
									
									
									
									
										Normal 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"]') | ||||
							
								
								
									
										23
									
								
								e2e/tests/dynamic/element/iframes/wait_class.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								e2e/tests/dynamic/element/iframes/wait_class.fql
									
									
									
									
									
										Normal 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 "" | ||||
| @@ -2,7 +2,7 @@ LET url = @dynamic | ||||
| LET doc = DOCUMENT(url, true) | ||||
| 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 r1 = '(\s|\")' | ||||
|   | ||||
| @@ -1,12 +1,9 @@ | ||||
| LET url = @static + '/value.html' | ||||
| LET url = @dynamic + "?redirect=/forms" | ||||
| 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 = ( | ||||
| 	FOR tr IN ELEMENTS(doc, '#listings_table > tbody > tr') | ||||
| 		LET elem = ELEMENT(tr, 'td > input') | ||||
| 		RETURN elem.value | ||||
| ) | ||||
| LET expected = "1" | ||||
| LET actual = el.value | ||||
|  | ||||
| RETURN EXPECT(actual, expected) | ||||
| @@ -1,4 +1,7 @@ | ||||
| LET url = @dynamic | ||||
| LET doc = DOCUMENT(url, true) | ||||
|  | ||||
| RETURN EXPECT(doc.url, url) | ||||
| LET expected = url + '/' | ||||
| LET actual = doc.url | ||||
|  | ||||
| RETURN EXPECT(expected, actual) | ||||
							
								
								
									
										5
									
								
								examples/iframes.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								examples/iframes.fql
									
									
									
									
									
										Normal 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 | ||||
| @@ -4,14 +4,15 @@ INPUT(amazon, '#twotabsearchtextbox', @criteria) | ||||
| CLICK(amazon, '.nav-search-submit input[type="submit"]') | ||||
| WAIT_NAVIGATION(amazon) | ||||
|  | ||||
| LET resultListSelector = '#s-results-list-atf' | ||||
| LET resultItemSelector = '.s-result-item.celwidget' | ||||
| LET nextBtnSelector = '#pagnNextLink' | ||||
| LET resultListSelector = 'div.s-result-list' | ||||
| LET resultItemSelector = 'div.s-result-item' | ||||
| LET nextBtnSelector = 'ul.a-pagination .a-last a' | ||||
| 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 priceWholeSelector = 'span.sx-price-whole' | ||||
| 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 = ( | ||||
|     FOR pageNum IN 1..pages | ||||
| @@ -19,6 +20,8 @@ LET result = ( | ||||
|         LET wait = clicked ? WAIT_NAVIGATION(amazon) : false | ||||
|         LET waitSelector = wait ? WAIT_ELEMENT(amazon, resultListSelector) : false | ||||
|  | ||||
|         PRINT("page:", pageNum, "clicked", clicked) | ||||
|  | ||||
|         LET items = ( | ||||
|             FOR el IN ELEMENTS(amazon, resultItemSelector) | ||||
|                 LET priceWholeTxt = INNER_TEXT(el, priceWholeSelector) | ||||
|   | ||||
							
								
								
									
										3
									
								
								examples/screenshot.fql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								examples/screenshot.fql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| LET data = SCREENSHOT("https://github.com/MontFerret/ferret/raw/master/assets/logo.png") | ||||
|  | ||||
| RETURN { type: "png", data } | ||||
| @@ -24,13 +24,13 @@ BAR | ||||
| 	}) | ||||
|  | ||||
| 	Convey("Should be possible to use multi line string with nested strings", t, func() { | ||||
| 		out := compiler.New(). | ||||
| 		compiler.New(). | ||||
| 			MustCompile(fmt.Sprintf(` | ||||
| RETURN %s<!DOCTYPE html> | ||||
| 		<html lang="en"> | ||||
| 		<head> | ||||
| 		<meta charset="UTF-8"> | ||||
| 		<title>Title</title> | ||||
| 		<title>GetTitle</title> | ||||
| 		</head> | ||||
| 		<body> | ||||
| 			Hello world | ||||
| @@ -43,7 +43,7 @@ RETURN %s<!DOCTYPE html> | ||||
| 		<html lang="en"> | ||||
| 		<head> | ||||
| 		<meta charset="UTF-8"> | ||||
| 		<title>Title</title> | ||||
| 		<title>GetTitle</title> | ||||
| 		</head> | ||||
| 		<body> | ||||
| 			Hello world | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1
									
								
								pkg/drivers/cdp/document_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								pkg/drivers/cdp/document_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| package cdp | ||||
| @@ -4,24 +4,21 @@ import ( | ||||
| 	"context" | ||||
| 	"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/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/rpcc" | ||||
| 	"github.com/mafredri/cdp/session" | ||||
| 	"github.com/pkg/errors" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/logging" | ||||
| ) | ||||
|  | ||||
| const DriverName = "cdp" | ||||
|  | ||||
| type Driver struct { | ||||
| 	sync.Mutex | ||||
| 	mu        sync.Mutex | ||||
| 	dev       *devtool.DevTools | ||||
| 	conn      *rpcc.Conn | ||||
| 	client    *cdp.Client | ||||
| @@ -42,7 +39,7 @@ func (drv *Driver) Name() string { | ||||
| 	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) | ||||
|  | ||||
| 	err := drv.init(ctx) | ||||
| @@ -58,20 +55,15 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	url := params.URL | ||||
|  | ||||
| 	if url == "" { | ||||
| 		url = BlankPageURL | ||||
| 	} | ||||
|  | ||||
| 	// Create a new target belonging to the browser context | ||||
| 	createTargetArgs := target.NewCreateTargetArgs(url) | ||||
| 	// Args for a new target belonging to the browser context | ||||
| 	createTargetArgs := target.NewCreateTargetArgs(BlankPageURL) | ||||
|  | ||||
| 	if !drv.options.KeepCookies && !params.KeepCookies { | ||||
| 		// Set it to an incognito mode | ||||
| 		createTargetArgs.SetBrowserContextID(drv.contextID) | ||||
| 	} | ||||
|  | ||||
| 	// New target | ||||
| 	createTarget, err := drv.client.Target.CreateTarget(ctx, createTargetArgs) | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -99,69 +91,16 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	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 { | ||||
| 			var ua string | ||||
|  | ||||
| 			if params.UserAgent != "" { | ||||
| 				ua = common.GetUserAgent(params.UserAgent) | ||||
| 			} else { | ||||
| 				ua = common.GetUserAgent(drv.options.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 | ||||
| 	if params.UserAgent == "" { | ||||
| 		params.UserAgent = drv.options.UserAgent | ||||
| 	} | ||||
|  | ||||
| 	return LoadHTMLDocument(ctx, conn, client, params) | ||||
| 	return LoadHTMLPage(ctx, conn, params) | ||||
| } | ||||
|  | ||||
| func (drv *Driver) Close() error { | ||||
| 	drv.Lock() | ||||
| 	defer drv.Unlock() | ||||
| 	drv.mu.Lock() | ||||
| 	defer drv.mu.Unlock() | ||||
|  | ||||
| 	if drv.session != nil { | ||||
| 		drv.session.Close() | ||||
| @@ -173,8 +112,8 @@ func (drv *Driver) Close() error { | ||||
| } | ||||
|  | ||||
| func (drv *Driver) init(ctx context.Context) error { | ||||
| 	drv.Lock() | ||||
| 	defer drv.Unlock() | ||||
| 	drv.mu.Lock() | ||||
| 	defer drv.mu.Unlock() | ||||
|  | ||||
| 	if drv.session == nil { | ||||
| 		ver, err := drv.dev.Version(ctx) | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"golang.org/x/net/html" | ||||
| 	"hash/fnv" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| @@ -40,25 +41,27 @@ type ( | ||||
| 		logger         *zerolog.Logger | ||||
| 		client         *cdp.Client | ||||
| 		events         *events.EventBroker | ||||
| 		exec           *eval.ExecutionContext | ||||
| 		connected      values.Boolean | ||||
| 		id             *HTMLElementIdentity | ||||
| 		nodeType       values.Int | ||||
| 		id             HTMLElementIdentity | ||||
| 		nodeType       html.NodeType | ||||
| 		nodeName       values.String | ||||
| 		innerHTML      values.String | ||||
| 		innerText      *common.LazyValue | ||||
| 		value          core.Value | ||||
| 		attributes     *common.LazyValue | ||||
| 		style          *common.LazyValue | ||||
| 		children       []*HTMLElementIdentity | ||||
| 		children       []HTMLElementIdentity | ||||
| 		loadedChildren *common.LazyValue | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func LoadElement( | ||||
| func LoadHTMLElement( | ||||
| 	ctx context.Context, | ||||
| 	logger *zerolog.Logger, | ||||
| 	client *cdp.Client, | ||||
| 	broker *events.EventBroker, | ||||
| 	exec *eval.ExecutionContext, | ||||
| 	nodeID dom.NodeID, | ||||
| 	backendID dom.BackendNodeID, | ||||
| ) (*HTMLElement, error) { | ||||
| @@ -99,7 +102,7 @@ func LoadElement( | ||||
| 		return nil, core.Error(err, strconv.Itoa(int(nodeID))) | ||||
| 	} | ||||
|  | ||||
| 	id := new(HTMLElementIdentity) | ||||
| 	id := HTMLElementIdentity{} | ||||
| 	id.nodeID = nodeID | ||||
| 	id.objectID = objectID | ||||
|  | ||||
| @@ -109,7 +112,7 @@ func LoadElement( | ||||
| 		id.backendID = node.Node.BackendNodeID | ||||
| 	} | ||||
|  | ||||
| 	innerHTML, err := loadInnerHTML(ctx, client, id) | ||||
| 	innerHTML, err := loadInnerHTML(ctx, client, exec, id, common.ToHTMLType(node.Node.NodeType)) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, core.Error(err, strconv.Itoa(int(nodeID))) | ||||
| @@ -125,6 +128,7 @@ func LoadElement( | ||||
| 		logger, | ||||
| 		client, | ||||
| 		broker, | ||||
| 		exec, | ||||
| 		id, | ||||
| 		node.Node.NodeType, | ||||
| 		node.Node.NodeName, | ||||
| @@ -138,20 +142,22 @@ func NewHTMLElement( | ||||
| 	logger *zerolog.Logger, | ||||
| 	client *cdp.Client, | ||||
| 	broker *events.EventBroker, | ||||
| 	id *HTMLElementIdentity, | ||||
| 	exec *eval.ExecutionContext, | ||||
| 	id HTMLElementIdentity, | ||||
| 	nodeType int, | ||||
| 	nodeName string, | ||||
| 	value string, | ||||
| 	innerHTML values.String, | ||||
| 	children []*HTMLElementIdentity, | ||||
| 	children []HTMLElementIdentity, | ||||
| ) *HTMLElement { | ||||
| 	el := new(HTMLElement) | ||||
| 	el.logger = logger | ||||
| 	el.client = client | ||||
| 	el.events = broker | ||||
| 	el.exec = exec | ||||
| 	el.connected = values.True | ||||
| 	el.id = id | ||||
| 	el.nodeType = values.NewInt(nodeType) | ||||
| 	el.nodeType = common.ToHTMLType(nodeType) | ||||
| 	el.nodeName = values.NewString(nodeName) | ||||
| 	el.innerHTML = innerHTML | ||||
| 	el.innerText = common.NewLazyValue(el.loadInnerText) | ||||
| @@ -207,7 +213,7 @@ func (el *HTMLElement) MarshalJSON() ([]byte, error) { | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| @@ -217,7 +223,7 @@ func (el *HTMLElement) Compare(other core.Value) int64 { | ||||
|  | ||||
| 		ctx := context.Background() | ||||
|  | ||||
| 		return el.InnerHTML(ctx).Compare(other.InnerHTML(ctx)) | ||||
| 		return el.GetInnerHTML(ctx).Compare(other.GetInnerHTML(ctx)) | ||||
| 	default: | ||||
| 		return drivers.Compare(el.Type(), other.Type()) | ||||
| 	} | ||||
| @@ -257,11 +263,11 @@ func (el *HTMLElement) SetIn(ctx context.Context, path []core.Value, value core. | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) GetValue(ctx context.Context) core.Value { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		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 { | ||||
| 		el.logError(err).Msg("failed to get node value") | ||||
| @@ -275,7 +281,7 @@ func (el *HTMLElement) GetValue(ctx context.Context) core.Value { | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) SetValue(ctx context.Context, value core.Value) error { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		// TODO: Return an error | ||||
| 		return nil | ||||
| 	} | ||||
| @@ -283,11 +289,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())) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) NodeType() values.Int { | ||||
| 	return el.nodeType | ||||
| func (el *HTMLElement) GetNodeType() values.Int { | ||||
| 	return values.NewInt(common.FromHTMLType(el.nodeType)) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) NodeName() values.String { | ||||
| func (el *HTMLElement) GetNodeName() values.String { | ||||
| 	return el.nodeName | ||||
| } | ||||
|  | ||||
| @@ -472,7 +478,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 { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		return values.None | ||||
| 	} | ||||
|  | ||||
| @@ -496,7 +502,7 @@ func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String | ||||
| 		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.exec, found.NodeID, emptyBackendID) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		el.logError(err). | ||||
| @@ -510,7 +516,7 @@ func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.String) core.Value { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		return values.NewArray(0) | ||||
| 	} | ||||
|  | ||||
| @@ -537,7 +543,7 @@ func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.Str | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		childEl, err := LoadElement(ctx, el.logger, el.client, el.events, id, emptyBackendID) | ||||
| 		childEl, err := LoadHTMLElement(ctx, el.logger, el.client, el.events, el.exec, id, emptyBackendID) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			el.logError(err). | ||||
| @@ -562,7 +568,7 @@ func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.Str | ||||
| 	return arr | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) InnerText(ctx context.Context) values.String { | ||||
| func (el *HTMLElement) GetInnerText(ctx context.Context) values.String { | ||||
| 	val, err := el.innerText.Read(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -577,7 +583,7 @@ func (el *HTMLElement) InnerText(ctx context.Context) values.String { | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) InnerTextBySelector(ctx context.Context, selector values.String) values.String { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		return values.EmptyString | ||||
| 	} | ||||
|  | ||||
| @@ -624,7 +630,7 @@ func (el *HTMLElement) InnerTextBySelector(ctx context.Context, selector values. | ||||
|  | ||||
| 	objID := *obj.Object.ObjectID | ||||
|  | ||||
| 	text, err := eval.Property(ctx, el.client, objID, "innerText") | ||||
| 	text, err := el.exec.ReadProperty(ctx, objID, "innerText") | ||||
|  | ||||
| 	if err != nil { | ||||
| 		el.logError(err). | ||||
| @@ -679,7 +685,7 @@ func (el *HTMLElement) InnerTextBySelectorAll(ctx context.Context, selector valu | ||||
|  | ||||
| 		objID := *obj.Object.ObjectID | ||||
|  | ||||
| 		text, err := eval.Property(ctx, el.client, objID, "innerText") | ||||
| 		text, err := el.exec.ReadProperty(ctx, objID, "innerText") | ||||
|  | ||||
| 		if err != nil { | ||||
| 			el.logError(err). | ||||
| @@ -696,7 +702,7 @@ func (el *HTMLElement) InnerTextBySelectorAll(ctx context.Context, selector valu | ||||
| 	return arr | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) InnerHTML(_ context.Context) values.String { | ||||
| func (el *HTMLElement) GetInnerHTML(_ context.Context) values.String { | ||||
| 	el.mu.Lock() | ||||
| 	defer el.mu.Unlock() | ||||
|  | ||||
| @@ -704,7 +710,7 @@ func (el *HTMLElement) InnerHTML(_ context.Context) values.String { | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) InnerHTMLBySelector(ctx context.Context, selector values.String) values.String { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		return values.EmptyString | ||||
| 	} | ||||
|  | ||||
| @@ -719,13 +725,12 @@ func (el *HTMLElement) InnerHTMLBySelector(ctx context.Context, selector values. | ||||
| 		return values.EmptyString | ||||
| 	} | ||||
|  | ||||
| 	text, err := loadInnerHTML(ctx, el.client, &HTMLElementIdentity{ | ||||
| 		nodeID: found.NodeID, | ||||
| 	}) | ||||
| 	text, err := loadInnerHTMLByNodeID(ctx, el.client, el.exec, found.NodeID) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		el.logError(err). | ||||
| 			Str("selector", selector.String()). | ||||
| 			Int("childNodeID", int(found.NodeID)). | ||||
| 			Msg("failed to load inner HTML for found child el") | ||||
|  | ||||
| 		return values.EmptyString | ||||
| @@ -750,13 +755,12 @@ func (el *HTMLElement) InnerHTMLBySelectorAll(ctx context.Context, selector valu | ||||
| 	arr := values.NewArray(len(res.NodeIDs)) | ||||
|  | ||||
| 	for _, id := range res.NodeIDs { | ||||
| 		text, err := loadInnerHTML(ctx, el.client, &HTMLElementIdentity{ | ||||
| 			nodeID: id, | ||||
| 		}) | ||||
| 		text, err := loadInnerHTMLByNodeID(ctx, el.client, el.exec, id) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			el.logError(err). | ||||
| 				Str("selector", selector.String()). | ||||
| 				Int("childNodeID", int(id)). | ||||
| 				Msg("failed to load inner HTML for found child el") | ||||
|  | ||||
| 			// return what we have | ||||
| @@ -770,7 +774,7 @@ func (el *HTMLElement) InnerHTMLBySelectorAll(ctx context.Context, selector valu | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.String) values.Int { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		return values.ZeroInt | ||||
| 	} | ||||
|  | ||||
| @@ -790,7 +794,7 @@ func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.Stri | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector values.String) values.Boolean { | ||||
| 	if !el.IsConnected() { | ||||
| 	if el.IsDetached() { | ||||
| 		return values.False | ||||
| 	} | ||||
|  | ||||
| @@ -887,7 +891,50 @@ func (el *HTMLElement) WaitForStyle(ctx context.Context, name values.String, val | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) Click(ctx context.Context) (values.Boolean, error) { | ||||
| 	return events.DispatchEvent(ctx, el.client, el.id.objectID, "click") | ||||
| 	if err := el.ScrollIntoView(ctx); err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	points, err := getClickablePoint(ctx, el.client, el.id) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	moveArgs := input.NewDispatchMouseEventArgs("mouseMoved", points.X, points.Y) | ||||
|  | ||||
| 	if err := el.client.Input.DispatchMouseEvent(ctx, moveArgs); err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	beforePressDelay := time.Duration(core.Random(100, 50)) | ||||
|  | ||||
| 	time.Sleep(beforePressDelay) | ||||
|  | ||||
| 	btn := "left" | ||||
| 	clickCount := 1 | ||||
|  | ||||
| 	downArgs := input.NewDispatchMouseEventArgs("mousePressed", points.X, points.Y) | ||||
| 	downArgs.ClickCount = &clickCount | ||||
| 	downArgs.Button = &btn | ||||
|  | ||||
| 	if err := el.client.Input.DispatchMouseEvent(ctx, downArgs); err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	beforeReleaseDelay := time.Duration(core.Random(50, 25)) | ||||
|  | ||||
| 	time.Sleep(beforeReleaseDelay * time.Millisecond) | ||||
|  | ||||
| 	upArgs := input.NewDispatchMouseEventArgs("mouseReleased", points.X, points.Y) | ||||
| 	upArgs.ClickCount = &clickCount | ||||
| 	upArgs.Button = &btn | ||||
|  | ||||
| 	if err := el.client.Input.DispatchMouseEvent(ctx, upArgs); err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	return values.True, nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) Input(ctx context.Context, value core.Value, delay values.Int) error { | ||||
| @@ -923,7 +970,7 @@ func (el *HTMLElement) Input(ctx context.Context, value core.Value, delay values | ||||
| func (el *HTMLElement) Select(ctx context.Context, value *values.Array) (*values.Array, error) { | ||||
| 	var attrID = "data-ferret-select" | ||||
|  | ||||
| 	if el.NodeName() != "SELECT" { | ||||
| 	if el.GetNodeName() != "SELECT" { | ||||
| 		return nil, core.Error(core.ErrInvalidOperation, "element is not a <select> element.") | ||||
| 	} | ||||
|  | ||||
| @@ -939,9 +986,8 @@ func (el *HTMLElement) Select(ctx context.Context, value *values.Array) (*values | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	res, err := eval.Eval( | ||||
| 	res, err := el.exec.EvalWithReturn( | ||||
| 		ctx, | ||||
| 		el.client, | ||||
| 		fmt.Sprintf(` | ||||
| 			var el = document.querySelector('[%s="%s"]'); | ||||
| 			if (el == null) { | ||||
| @@ -969,8 +1015,6 @@ func (el *HTMLElement) Select(ctx context.Context, value *values.Array) (*values | ||||
| 			id.String(), | ||||
| 			value.String(), | ||||
| 		), | ||||
| 		true, | ||||
| 		false, | ||||
| 	) | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -1007,9 +1051,8 @@ func (el *HTMLElement) ScrollIntoView(ctx context.Context) error { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, err = eval.Eval( | ||||
| 	err = el.exec.Eval( | ||||
| 		ctx, | ||||
| 		el.client, | ||||
| 		fmt.Sprintf(` | ||||
| 			var el = document.querySelector('[%s="%s"]'); | ||||
| 			if (el == null) { | ||||
| @@ -1024,7 +1067,7 @@ func (el *HTMLElement) ScrollIntoView(ctx context.Context) error { | ||||
| 		`, | ||||
| 			attrID, | ||||
| 			id.String(), | ||||
| 		), false, false) | ||||
| 		)) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -1054,16 +1097,16 @@ func (el *HTMLElement) Hover(ctx context.Context) error { | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) IsConnected() values.Boolean { | ||||
| func (el *HTMLElement) IsDetached() values.Boolean { | ||||
| 	el.mu.Lock() | ||||
| 	defer el.mu.Unlock() | ||||
|  | ||||
| 	return el.connected | ||||
| 	return !el.connected | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) { | ||||
| 	if el.IsConnected() { | ||||
| 		text, err := loadInnerText(ctx, el.client, el.id) | ||||
| 	if !el.IsDetached() { | ||||
| 		text, err := loadInnerText(ctx, el.client, el.exec, el.id, el.nodeType) | ||||
|  | ||||
| 		if err == nil { | ||||
| 			return text, nil | ||||
| @@ -1074,7 +1117,7 @@ func (el *HTMLElement) loadInnerText(ctx context.Context) (core.Value, error) { | ||||
| 		// and just parse cached innerHTML | ||||
| 	} | ||||
|  | ||||
| 	h := el.InnerHTML(ctx) | ||||
| 	h := el.GetInnerHTML(ctx) | ||||
|  | ||||
| 	if h == values.EmptyString { | ||||
| 		return h, nil | ||||
| @@ -1102,18 +1145,19 @@ func (el *HTMLElement) loadAttrs(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 | ||||
| 	} | ||||
|  | ||||
| 	loaded := values.NewArray(len(el.children)) | ||||
|  | ||||
| 	for _, childID := range el.children { | ||||
| 		child, err := LoadElement( | ||||
| 		child, err := LoadHTMLElement( | ||||
| 			ctx, | ||||
| 			el.logger, | ||||
| 			el.client, | ||||
| 			el.events, | ||||
| 			el.exec, | ||||
| 			childID.nodeID, | ||||
| 			childID.backendID, | ||||
| 		) | ||||
| @@ -1285,21 +1329,21 @@ func (el *HTMLElement) handleChildInserted(ctx context.Context, message interfac | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	nextIdentity := &HTMLElementIdentity{ | ||||
| 	nextIdentity := HTMLElementIdentity{ | ||||
| 		nodeID:    reply.Node.NodeID, | ||||
| 		backendID: reply.Node.BackendNodeID, | ||||
| 	} | ||||
|  | ||||
| 	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() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	el.loadedChildren.Write(ctx, func(v core.Value, err error) { | ||||
| 	el.loadedChildren.Write(ctx, func(v core.Value, _ error) { | ||||
| 		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.exec, nextID, emptyBackendID) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			el.logError(err).Msg("failed to load an inserted element") | ||||
| @@ -1309,7 +1353,7 @@ func (el *HTMLElement) handleChildInserted(ctx context.Context, message interfac | ||||
|  | ||||
| 		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 { | ||||
| 			el.logError(err).Msg("failed to update element") | ||||
| @@ -1371,7 +1415,7 @@ func (el *HTMLElement) handleChildRemoved(ctx context.Context, message interface | ||||
| 		loadedArr := v.(*values.Array) | ||||
| 		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 { | ||||
| 			el.logger.Error(). | ||||
|   | ||||
							
								
								
									
										217
									
								
								pkg/drivers/cdp/eval/context.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								pkg/drivers/cdp/eval/context.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,217 @@ | ||||
| package eval | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"github.com/mafredri/cdp" | ||||
| 	"github.com/mafredri/cdp/protocol/page" | ||||
| 	"github.com/mafredri/cdp/protocol/runtime" | ||||
|  | ||||
| 	"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.eval( | ||||
| 		ctx, | ||||
| 		runtime. | ||||
| 			NewEvaluateArgs(PrepareEval(exp)), | ||||
| 	) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (ec *ExecutionContext) EvalWithReturn(ctx context.Context, exp string) (core.Value, error) { | ||||
| 	return ec.eval( | ||||
| 		ctx, | ||||
| 		runtime. | ||||
| 			NewEvaluateArgs(PrepareEval(exp)). | ||||
| 			SetReturnByValue(true), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func (ec *ExecutionContext) EvalAsync(ctx context.Context, exp string) (core.Value, error) { | ||||
| 	return ec.eval( | ||||
| 		ctx, | ||||
| 		runtime. | ||||
| 			NewEvaluateArgs(PrepareEval(exp)). | ||||
| 			SetReturnByValue(true). | ||||
| 			SetAwaitPromise(true), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func (ec *ExecutionContext) eval(ctx context.Context, args *runtime.EvaluateArgs) (core.Value, error) { | ||||
| 	if ec.contextID != EmptyExecutionContextID { | ||||
| 		args.SetContextID(ec.contextID) | ||||
| 	} | ||||
|  | ||||
| 	out, err := ec.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" && out.Result.Type != "null" { | ||||
| 		return values.Unmarshal(out.Result.Value) | ||||
| 	} | ||||
|  | ||||
| 	return Unmarshal(&out.Result) | ||||
| } | ||||
|  | ||||
| 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 | ||||
| } | ||||
| @@ -1,159 +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 == "" { | ||||
| 		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 | ||||
| } | ||||
							
								
								
									
										35
									
								
								pkg/drivers/cdp/eval/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								pkg/drivers/cdp/eval/helpers.go
									
									
									
									
									
										Normal 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) | ||||
| 	} | ||||
| } | ||||
| @@ -179,6 +179,16 @@ func (broker *EventBroker) Close() error { | ||||
| 	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) { | ||||
| 	for { | ||||
| 		select { | ||||
| @@ -265,6 +275,7 @@ func (broker *EventBroker) emit(ctx context.Context, event Event, message interf | ||||
| 	listeners, ok := broker.listeners[event] | ||||
|  | ||||
| 	if !ok { | ||||
| 		broker.mu.Unlock() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
							
								
								
									
										126
									
								
								pkg/drivers/cdp/events/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								pkg/drivers/cdp/events/helpers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| package events | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/mafredri/cdp/protocol/dom" | ||||
| 	"github.com/mafredri/cdp/protocol/page" | ||||
| 	"github.com/pkg/errors" | ||||
|  | ||||
| 	"github.com/mafredri/cdp" | ||||
| ) | ||||
|  | ||||
| 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 | ||||
| } | ||||
| @@ -2,12 +2,12 @@ package events | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/mafredri/cdp" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type ( | ||||
| @@ -58,18 +58,15 @@ func (task *WaitTask) Run(ctx context.Context) (core.Value, error) { | ||||
| } | ||||
|  | ||||
| func NewEvalWaitTask( | ||||
| 	client *cdp.Client, | ||||
| 	ec *eval.ExecutionContext, | ||||
| 	predicate string, | ||||
| 	polling time.Duration, | ||||
| ) *WaitTask { | ||||
| 	return NewWaitTask( | ||||
| 		func(ctx context.Context) (core.Value, error) { | ||||
| 			return eval.Eval( | ||||
| 			return ec.EvalWithReturn( | ||||
| 				ctx, | ||||
| 				client, | ||||
| 				predicate, | ||||
| 				true, | ||||
| 				false, | ||||
| 			) | ||||
| 		}, | ||||
| 		polling, | ||||
|   | ||||
| @@ -4,13 +4,13 @@ import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"golang.org/x/net/html" | ||||
| 	"math" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/cdp/eval" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/cdp/events" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/common" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| @@ -44,16 +44,6 @@ func runBatch(funcs ...batchFunc) error { | ||||
| 	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{ | ||||
| 		{ | ||||
| @@ -87,7 +77,20 @@ func computeQuadArea(quads []Quad) float64 { | ||||
| 	return math.Abs(area) | ||||
| } | ||||
|  | ||||
| func getClickablePoint(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (Quad, error) { | ||||
| 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, id HTMLElementIdentity) (Quad, error) { | ||||
| 	qargs := dom.NewGetContentQuadsArgs() | ||||
|  | ||||
| 	switch { | ||||
| @@ -99,20 +102,29 @@ func getClickablePoint(ctx context.Context, client *cdp.Client, id *HTMLElementI | ||||
| 		qargs.SetNodeID(id.nodeID) | ||||
| 	} | ||||
|  | ||||
| 	res, err := client.DOM.GetContentQuads(ctx, qargs) | ||||
| 	contentQuadsReply, err := client.DOM.GetContentQuads(ctx, qargs) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return Quad{}, err | ||||
| 	} | ||||
|  | ||||
| 	if res.Quads == nil || len(res.Quads) == 0 { | ||||
| 	if contentQuadsReply.Quads == nil || len(contentQuadsReply.Quads) == 0 { | ||||
| 		return Quad{}, errors.New("node is either not visible or not an HTMLElement") | ||||
| 	} | ||||
|  | ||||
| 	quads := make([][]Quad, 0, len(res.Quads)) | ||||
| 	layoutMetricsReply, err := client.Page.GetLayoutMetrics(ctx) | ||||
|  | ||||
| 	for _, q := range res.Quads { | ||||
| 		quad := fromProtocolQuad(q) | ||||
| 	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) | ||||
| @@ -167,41 +179,105 @@ func parseAttrs(attrs []string) *values.Object { | ||||
| 	return res | ||||
| } | ||||
|  | ||||
| func loadInnerHTML(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (values.String, error) { | ||||
| 	var objID runtime.RemoteObjectID | ||||
| 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 | ||||
|  | ||||
| 	switch { | ||||
| 	case id.objectID != "": | ||||
| 		objID = id.objectID | ||||
| 	case id.backendID > 0: | ||||
| 		repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetBackendNodeID(id.backendID)) | ||||
| 		switch { | ||||
| 		case id.objectID != "": | ||||
| 			objID = id.objectID | ||||
| 		case id.backendID > 0: | ||||
| 			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)) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return "", err | ||||
| 			} | ||||
|  | ||||
| 			if repl.Object.ObjectID == nil { | ||||
| 				return "", errors.New("unable to resolve node") | ||||
| 			} | ||||
|  | ||||
| 			objID = *repl.Object.ObjectID | ||||
| 		} | ||||
|  | ||||
| 		res, err := exec.ReadProperty(ctx, objID, "innerHTML") | ||||
|  | ||||
| 		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)) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		if repl.Object.ObjectID == nil { | ||||
| 			return "", errors.New("unable to resolve node") | ||||
| 		} | ||||
|  | ||||
| 		objID = *repl.Object.ObjectID | ||||
| 		return values.NewString(res.String()), nil | ||||
| 	} | ||||
|  | ||||
| 	repl, err := exec.EvalWithReturn(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 | ||||
| 	if id.nodeID != 1 { | ||||
| 		res, err := eval.Property(ctx, client, objID, "innerHTML") | ||||
| 	if nodeType != html.DocumentNode { | ||||
| 		var objID runtime.RemoteObjectID | ||||
|  | ||||
| 		switch { | ||||
| 		case id.objectID != "": | ||||
| 			objID = id.objectID | ||||
| 		case id.backendID > 0: | ||||
| 			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)) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return "", err | ||||
| 			} | ||||
|  | ||||
| 			if repl.Object.ObjectID == nil { | ||||
| 				return "", errors.New("unable to resolve node") | ||||
| 			} | ||||
|  | ||||
| 			objID = *repl.Object.ObjectID | ||||
| 		} | ||||
|  | ||||
| 		res, err := exec.ReadProperty(ctx, objID, "innerText") | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| @@ -210,66 +286,26 @@ func loadInnerHTML(ctx context.Context, client *cdp.Client, id *HTMLElementIdent | ||||
| 		return values.NewString(res.String()), err | ||||
| 	} | ||||
|  | ||||
| 	repl, err := client.DOM.GetOuterHTML(ctx, dom.NewGetOuterHTMLArgs().SetObjectID(objID)) | ||||
| 	repl, err := exec.EvalWithReturn(ctx, "return document.documentElement.innerText") | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return values.NewString(repl.OuterHTML), nil | ||||
| 	return values.NewString(repl.String()), nil | ||||
| } | ||||
|  | ||||
| func loadInnerText(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (values.String, error) { | ||||
| 	var objID runtime.RemoteObjectID | ||||
|  | ||||
| 	switch { | ||||
| 	case id.objectID != "": | ||||
| 		objID = id.objectID | ||||
| 	case id.backendID > 0: | ||||
| 		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)) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
|  | ||||
| 		if repl.Object.ObjectID == nil { | ||||
| 			return "", errors.New("unable to resolve node") | ||||
| 		} | ||||
|  | ||||
| 		objID = *repl.Object.ObjectID | ||||
| 	} | ||||
|  | ||||
| 	// not a document | ||||
| 	if id.nodeID != 1 { | ||||
| 		res, err := eval.Property(ctx, client, objID, "innerText") | ||||
|  | ||||
| 		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 parseInnerText(repl.OuterHTML) | ||||
| } | ||||
| //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) { | ||||
| 	buff := bytes.NewBuffer([]byte(innerHTML)) | ||||
| @@ -283,11 +319,12 @@ func parseInnerText(innerHTML string) (values.String, error) { | ||||
| 	return values.NewString(parsed.Text()), nil | ||||
| } | ||||
|  | ||||
| func createChildrenArray(nodes []dom.Node) []*HTMLElementIdentity { | ||||
| 	children := make([]*HTMLElementIdentity, len(nodes)) | ||||
| func createChildrenArray(nodes []dom.Node) []HTMLElementIdentity { | ||||
| 	children := make([]HTMLElementIdentity, len(nodes)) | ||||
|  | ||||
| 	for idx, child := range nodes { | ||||
| 		children[idx] = &HTMLElementIdentity{ | ||||
| 		child := child | ||||
| 		children[idx] = HTMLElementIdentity{ | ||||
| 			nodeID:    child.NodeID, | ||||
| 			backendID: child.BackendNodeID, | ||||
| 		} | ||||
| @@ -296,122 +333,6 @@ func createChildrenArray(nodes []dom.Node) []*HTMLElementIdentity { | ||||
| 	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 { | ||||
| 	sameSite := network.CookieSameSiteNotSet | ||||
|  | ||||
| @@ -491,3 +412,59 @@ func randomDuration(delay values.Int) time.Duration { | ||||
|  | ||||
| 	return time.Duration(int64(value)) | ||||
| } | ||||
|  | ||||
| func resolveFrame(ctx context.Context, client *cdp.Client, frame page.Frame) (dom.Node, runtime.ExecutionContextID, error) { | ||||
| 	worldRepl, err := client.Page.CreateIsolatedWorld(ctx, page.NewCreateIsolatedWorldArgs(frame.ID)) | ||||
|  | ||||
| 	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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										751
									
								
								pkg/drivers/cdp/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										751
									
								
								pkg/drivers/cdp/page.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,751 @@ | ||||
| package cdp | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers/common" | ||||
| 	"github.com/mafredri/cdp/protocol/emulation" | ||||
| 	"github.com/mafredri/cdp/protocol/page" | ||||
| 	"hash/fnv" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/mafredri/cdp" | ||||
| 	"github.com/mafredri/cdp/protocol/network" | ||||
| 	"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/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 | ||||
| 	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") | ||||
| 	} | ||||
|  | ||||
| 	doc, err := LoadRootHTMLDocument(ctx, logger, client, broker) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		broker.StopAndClose() | ||||
| 		handleLoadError(logger, client) | ||||
|  | ||||
| 		return nil, errors.Wrap(err, "failed to load root element") | ||||
| 	} | ||||
|  | ||||
| 	return NewHTMLPage( | ||||
| 		logger, | ||||
| 		conn, | ||||
| 		client, | ||||
| 		broker, | ||||
| 		doc, | ||||
| 	), nil | ||||
| } | ||||
|  | ||||
| func NewHTMLPage( | ||||
| 	logger *zerolog.Logger, | ||||
| 	conn *rpcc.Conn, | ||||
| 	client *cdp.Client, | ||||
| 	broker *events.EventBroker, | ||||
| 	document *HTMLDocument, | ||||
| ) *HTMLPage { | ||||
| 	p := new(HTMLPage) | ||||
| 	p.closed = values.False | ||||
| 	p.logger = logger | ||||
| 	p.conn = conn | ||||
| 	p.client = client | ||||
| 	p.events = broker | ||||
| 	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) { | ||||
| 	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) { | ||||
| 	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 { | ||||
| 	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) { | ||||
| 	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) { | ||||
| 	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) | ||||
|  | ||||
| 	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 | ||||
| } | ||||
							
								
								
									
										32
									
								
								pkg/drivers/common/frames.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								pkg/drivers/common/frames.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| 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 | ||||
| } | ||||
| @@ -9,9 +9,81 @@ import ( | ||||
| 	"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) { | ||||
| 	if len(path) == 0 { | ||||
| 		return values.None, nil | ||||
| 		return doc, nil | ||||
| 	} | ||||
|  | ||||
| 	segment := path[0] | ||||
| @@ -22,38 +94,49 @@ func GetInDocument(ctx context.Context, doc drivers.HTMLDocument, path []core.Va | ||||
| 		switch segment { | ||||
| 		case "url", "URL": | ||||
| 			return doc.GetURL(), nil | ||||
| 		case "cookies": | ||||
| 		case "title": | ||||
| 			return doc.GetTitle(), nil | ||||
| 		case "parent": | ||||
| 			parent := doc.GetParentDocument() | ||||
|  | ||||
| 			if parent == nil { | ||||
| 				return values.None, nil | ||||
| 			} | ||||
|  | ||||
| 			if len(path) == 1 { | ||||
| 				return doc.GetCookies(ctx) | ||||
| 				return parent, nil | ||||
| 			} | ||||
|  | ||||
| 			switch idx := path[1].(type) { | ||||
| 			case values.Int: | ||||
| 				cookies, err := doc.GetCookies(ctx) | ||||
| 			return GetInDocument(ctx, parent, path[1:]) | ||||
| 		case "body", "head": | ||||
| 			out := doc.QuerySelector(ctx, segment) | ||||
|  | ||||
| 				if err != nil { | ||||
| 					return values.None, err | ||||
| 				} | ||||
|  | ||||
| 				return cookies.Get(idx), nil | ||||
| 			default: | ||||
| 				return values.None, core.TypeError(idx.Type(), types.Int) | ||||
| 			if out == values.None { | ||||
| 				return out, nil | ||||
| 			} | ||||
| 		case "body": | ||||
| 			return doc.QuerySelector(ctx, "body"), nil | ||||
| 		case "head": | ||||
| 			return doc.QuerySelector(ctx, "head"), nil | ||||
|  | ||||
| 			if len(path) == 1 { | ||||
| 				return out, nil | ||||
| 			} | ||||
|  | ||||
| 			el, err := drivers.ToElement(out) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return values.None, err | ||||
| 			} | ||||
|  | ||||
| 			return GetInElement(ctx, el, path[1:]) | ||||
| 		default: | ||||
| 			return GetInNode(ctx, doc.DocumentElement(), path) | ||||
| 			return GetInNode(ctx, doc.GetElement(), 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) { | ||||
| 	if len(path) == 0 { | ||||
| 		return values.None, nil | ||||
| 		return el, nil | ||||
| 	} | ||||
|  | ||||
| 	segment := path[0] | ||||
| @@ -63,9 +146,9 @@ func GetInElement(ctx context.Context, el drivers.HTMLElement, path []core.Value | ||||
|  | ||||
| 		switch segment { | ||||
| 		case "innerText": | ||||
| 			return el.InnerText(ctx), nil | ||||
| 			return el.GetInnerText(ctx), nil | ||||
| 		case "innerHTML": | ||||
| 			return el.InnerHTML(ctx), nil | ||||
| 			return el.GetInnerHTML(ctx), nil | ||||
| 		case "value": | ||||
| 			return el.GetValue(ctx), nil | ||||
| 		case "attributes": | ||||
| @@ -98,7 +181,7 @@ 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) { | ||||
| 	if len(path) == 0 { | ||||
| 		return values.None, nil | ||||
| 		return node, nil | ||||
| 	} | ||||
|  | ||||
| 	nt := node.Type() | ||||
| @@ -118,10 +201,12 @@ func GetInNode(ctx context.Context, node drivers.HTMLNode, path []core.Value) (c | ||||
| 		segment := segment.(values.String) | ||||
|  | ||||
| 		switch segment { | ||||
| 		case "isDetached": | ||||
| 			return node.IsDetached(), nil | ||||
| 		case "nodeType": | ||||
| 			return node.NodeType(), nil | ||||
| 			return node.GetNodeType(), nil | ||||
| 		case "nodeName": | ||||
| 			return node.NodeName(), nil | ||||
| 			return node.GetNodeName(), nil | ||||
| 		case "children": | ||||
| 			children := node.GetChildNodes(ctx) | ||||
|  | ||||
|   | ||||
| @@ -12,7 +12,7 @@ type ( | ||||
| 	LazyValueFactory func(ctx context.Context) (core.Value, error) | ||||
|  | ||||
| 	LazyValue struct { | ||||
| 		sync.Mutex | ||||
| 		mu      sync.Mutex | ||||
| 		factory LazyValueFactory | ||||
| 		ready   bool | ||||
| 		value   core.Value | ||||
| @@ -32,8 +32,8 @@ func NewLazyValue(factory LazyValueFactory) *LazyValue { | ||||
| // Ready indicates whether the value is ready. | ||||
| // @returns (Boolean) - Boolean value indicating whether the value is ready. | ||||
| func (lv *LazyValue) Ready() bool { | ||||
| 	lv.Lock() | ||||
| 	defer lv.Unlock() | ||||
| 	lv.mu.Lock() | ||||
| 	defer lv.mu.Unlock() | ||||
|  | ||||
| 	return lv.ready | ||||
| } | ||||
| @@ -42,8 +42,8 @@ func (lv *LazyValue) Ready() bool { | ||||
| // Not thread safe. Should not mutated. | ||||
| // @returns (Value) - Underlying value if successfully loaded, otherwise error | ||||
| func (lv *LazyValue) Read(ctx context.Context) (core.Value, error) { | ||||
| 	lv.Lock() | ||||
| 	defer lv.Unlock() | ||||
| 	lv.mu.Lock() | ||||
| 	defer lv.mu.Unlock() | ||||
|  | ||||
| 	if !lv.ready { | ||||
| 		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. | ||||
| // Thread safe. | ||||
| func (lv *LazyValue) Write(ctx context.Context, writer func(v core.Value, err error)) { | ||||
| 	lv.Lock() | ||||
| 	defer lv.Unlock() | ||||
| 	lv.mu.Lock() | ||||
| 	defer lv.mu.Unlock() | ||||
|  | ||||
| 	if !lv.ready { | ||||
| 		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. | ||||
| // Next call of Read will trigger the factory function again. | ||||
| func (lv *LazyValue) Reset() { | ||||
| 	lv.Lock() | ||||
| 	defer lv.Unlock() | ||||
| 	lv.mu.Lock() | ||||
| 	defer lv.mu.Unlock() | ||||
|  | ||||
| 	lv.ready = false | ||||
| 	lv.value = values.None | ||||
|   | ||||
| @@ -9,24 +9,17 @@ import ( | ||||
| 	"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 len(path) == 0 { | ||||
| 		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) | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| 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 { | ||||
| 	case html.DocumentNode: | ||||
| 		return 9 | ||||
| @@ -18,3 +20,20 @@ func ToHTMLType(nt html.NodeType) int { | ||||
|  | ||||
| 	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 | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -18,7 +18,7 @@ type ( | ||||
| 		drivers map[string]Driver | ||||
| 	} | ||||
|  | ||||
| 	LoadDocumentParams struct { | ||||
| 	OpenPageParams struct { | ||||
| 		URL         string | ||||
| 		UserAgent   string | ||||
| 		KeepCookies bool | ||||
| @@ -29,7 +29,7 @@ type ( | ||||
| 	Driver interface { | ||||
| 		io.Closer | ||||
| 		Name() string | ||||
| 		LoadDocument(ctx context.Context, params LoadDocumentParams) (HTMLDocument, error) | ||||
| 		Open(ctx context.Context, params OpenPageParams) (HTMLPage, error) | ||||
| 	} | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -2,8 +2,52 @@ package drivers | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| ) | ||||
|  | ||||
| func WithDefaultTimeout(ctx context.Context) (context.Context, context.CancelFunc) { | ||||
| 	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, | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -12,17 +12,25 @@ import ( | ||||
| ) | ||||
|  | ||||
| type HTMLDocument struct { | ||||
| 	docNode *goquery.Document | ||||
| 	element drivers.HTMLElement | ||||
| 	url     values.String | ||||
| 	cookies []drivers.HTTPCookie | ||||
| 	doc      *goquery.Document | ||||
| 	element  drivers.HTMLElement | ||||
| 	url      values.String | ||||
| 	parent   drivers.HTMLDocument | ||||
| 	children *values.Array | ||||
| } | ||||
|  | ||||
| func NewRootHTMLDocument( | ||||
| 	node *goquery.Document, | ||||
| 	url string, | ||||
| ) (*HTMLDocument, error) { | ||||
| 	return NewHTMLDocument(node, url, nil) | ||||
| } | ||||
|  | ||||
| func NewHTMLDocument( | ||||
| 	node *goquery.Document, | ||||
| 	url string, | ||||
| 	cookies []drivers.HTTPCookie, | ||||
| ) (drivers.HTMLDocument, error) { | ||||
| 	parent drivers.HTMLDocument, | ||||
| ) (*HTMLDocument, error) { | ||||
| 	if url == "" { | ||||
| 		return nil, core.Error(core.ErrMissedArgument, "document url") | ||||
| 	} | ||||
| @@ -37,7 +45,21 @@ func NewHTMLDocument( | ||||
| 		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) { | ||||
| @@ -49,7 +71,7 @@ func (doc *HTMLDocument) Type() core.Type { | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) String() string { | ||||
| 	str, err := doc.docNode.Html() | ||||
| 	str, err := doc.doc.Html() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return "" | ||||
| @@ -70,7 +92,7 @@ func (doc *HTMLDocument) Compare(other core.Value) int64 { | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) Unwrap() interface{} { | ||||
| 	return doc.docNode | ||||
| 	return doc.doc | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) Hash() uint64 { | ||||
| @@ -84,7 +106,7 @@ func (doc *HTMLDocument) Hash() uint64 { | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| 		return values.None | ||||
| @@ -94,24 +116,17 @@ func (doc *HTMLDocument) Copy() core.Value { | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) Clone() core.Value { | ||||
| 	var cookies []drivers.HTTPCookie | ||||
|  | ||||
| 	if doc.cookies != nil { | ||||
| 		cookies = make([]drivers.HTTPCookie, len(doc.cookies)) | ||||
| 		copy(cookies, doc.cookies) | ||||
| 	} | ||||
|  | ||||
| 	cp, err := NewHTMLDocument(goquery.CloneDocument(doc.docNode), string(doc.url), cookies) | ||||
| 	cloned, err := NewHTMLDocument(doc.doc, doc.url.String(), doc.parent) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None | ||||
| 	} | ||||
|  | ||||
| 	return cp | ||||
| 	return cloned | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| @@ -126,11 +141,11 @@ func (doc *HTMLDocument) SetIn(ctx context.Context, path []core.Value, value cor | ||||
| 	return common.SetInDocument(ctx, doc, path, value) | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) NodeType() values.Int { | ||||
| func (doc *HTMLDocument) GetNodeType() values.Int { | ||||
| 	return 9 | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) NodeName() values.String { | ||||
| func (doc *HTMLDocument) GetNodeName() values.String { | ||||
| 	return "#document" | ||||
| } | ||||
|  | ||||
| @@ -158,50 +173,34 @@ func (doc *HTMLDocument) ExistsBySelector(ctx context.Context, selector values.S | ||||
| 	return doc.element.ExistsBySelector(ctx, selector) | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) DocumentElement() drivers.HTMLElement { | ||||
| 	return doc.element | ||||
| func (doc *HTMLDocument) IsDetached() values.Boolean { | ||||
| 	return values.False | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) GetURL() core.Value { | ||||
| 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 | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) SetURL(_ context.Context, _ values.String) error { | ||||
| 	return core.ErrInvalidOperation | ||||
| func (doc *HTMLDocument) GetElement() drivers.HTMLElement { | ||||
| 	return doc.element | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) GetCookies(_ context.Context) (*values.Array, error) { | ||||
| 	if doc.cookies == nil { | ||||
| 		return values.NewArray(0), nil | ||||
| 	} | ||||
|  | ||||
| 	arr := values.NewArray(len(doc.cookies)) | ||||
|  | ||||
| 	for _, c := range doc.cookies { | ||||
| 		arr.Push(c) | ||||
| 	} | ||||
|  | ||||
| 	return arr, nil | ||||
| func (doc *HTMLDocument) GetName() values.String { | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| 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) GetParentDocument() drivers.HTMLDocument { | ||||
| 	return doc.parent | ||||
| } | ||||
|  | ||||
| func (doc *HTMLDocument) ClickBySelector(_ context.Context, _ values.String) (values.Boolean, error) { | ||||
|   | ||||
| @@ -220,7 +220,7 @@ func TestDocument(t *testing.T) { | ||||
|     </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> | ||||
| 	` | ||||
| 	Convey(".NodeType", t, func() { | ||||
| 	Convey(".GetNodeType", t, func() { | ||||
| 		Convey("Should serialize a boolean value", func() { | ||||
| 			buff := bytes.NewBuffer([]byte(doc)) | ||||
|  | ||||
| @@ -234,7 +234,7 @@ func TestDocument(t *testing.T) { | ||||
|  | ||||
| 			So(err, ShouldBeNil) | ||||
|  | ||||
| 			So(el.NodeType(), ShouldEqual, 9) | ||||
| 			So(el.GetNodeType(), ShouldEqual, 9) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -62,7 +62,7 @@ func (drv *Driver) Name() string { | ||||
| 	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) | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -133,10 +133,10 @@ func (drv *Driver) LoadDocument(ctx context.Context, params drivers.LoadDocument | ||||
| 		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)) | ||||
|  | ||||
| 	doc, err := goquery.NewDocumentFromReader(buf) | ||||
| @@ -145,7 +145,7 @@ func (drv *Driver) ParseDocument(_ context.Context, str values.String) (drivers. | ||||
| 		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 { | ||||
|   | ||||
| @@ -29,7 +29,7 @@ func NewHTMLElement(node *goquery.Selection) (drivers.HTMLElement, 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 { | ||||
| @@ -37,7 +37,7 @@ func (el *HTMLElement) Type() core.Type { | ||||
| } | ||||
|  | ||||
| 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 { | ||||
| @@ -48,7 +48,7 @@ func (el *HTMLElement) Compare(other core.Value) int64 { | ||||
| 		ctx, fn := drivers.WithDefaultTimeout(context.Background()) | ||||
| 		defer fn() | ||||
|  | ||||
| 		return el.InnerHTML(ctx).Compare(other.InnerHTML(ctx)) | ||||
| 		return el.GetInnerHTML(ctx).Compare(other.GetInnerHTML(ctx)) | ||||
| 	default: | ||||
| 		return drivers.Compare(el.Type(), other.Type()) | ||||
| 	} | ||||
| @@ -80,21 +80,25 @@ func (el *HTMLElement) Copy() core.Value { | ||||
| 	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 | ||||
|  | ||||
| 	if len(nodes) == 0 { | ||||
| 		return 0 | ||||
| 	} | ||||
|  | ||||
| 	return values.NewInt(common.ToHTMLType(nodes[0].Type)) | ||||
| 	return values.NewInt(common.FromHTMLType(nodes[0].Type)) | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) Close() error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) NodeName() values.String { | ||||
| func (el *HTMLElement) GetNodeName() values.String { | ||||
| 	return values.NewString(goquery.NodeName(el.selection)) | ||||
| } | ||||
|  | ||||
| @@ -122,11 +126,11 @@ func (el *HTMLElement) SetValue(_ context.Context, value core.Value) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (el *HTMLElement) InnerText(_ context.Context) values.String { | ||||
| func (el *HTMLElement) GetInnerText(_ context.Context) values.String { | ||||
| 	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() | ||||
|  | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -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> | ||||
| ` | ||||
|  | ||||
| 	Convey(".NodeType", t, func() { | ||||
| 	Convey(".GetNodeType", t, func() { | ||||
| 		buff := bytes.NewBuffer([]byte(doc)) | ||||
|  | ||||
| 		buff.Write([]byte(doc)) | ||||
| @@ -257,10 +257,10 @@ func TestElement(t *testing.T) { | ||||
|  | ||||
| 		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.Write([]byte(doc)) | ||||
| @@ -273,7 +273,7 @@ func TestElement(t *testing.T) { | ||||
|  | ||||
| 		So(err, ShouldBeNil) | ||||
|  | ||||
| 		So(el.NodeName(), ShouldEqual, "body") | ||||
| 		So(el.GetNodeName(), ShouldEqual, "body") | ||||
| 	}) | ||||
|  | ||||
| 	Convey(".Length", t, func() { | ||||
| @@ -327,7 +327,7 @@ func TestElement(t *testing.T) { | ||||
| 		So(v, ShouldEqual, "find") | ||||
| 	}) | ||||
|  | ||||
| 	Convey(".InnerText", t, func() { | ||||
| 	Convey(".GetInnerText", t, func() { | ||||
| 		buff := bytes.NewBuffer([]byte(` | ||||
| 			<html> | ||||
| 				<head></head> | ||||
| @@ -349,7 +349,7 @@ func TestElement(t *testing.T) { | ||||
|  | ||||
| 		So(err, ShouldBeNil) | ||||
|  | ||||
| 		v := el.InnerText(context.Background()) | ||||
| 		v := el.GetInnerText(context.Background()) | ||||
|  | ||||
| 		So(v, ShouldEqual, "Ferret") | ||||
| 	}) | ||||
| @@ -376,7 +376,7 @@ func TestElement(t *testing.T) { | ||||
|  | ||||
| 		So(err, ShouldBeNil) | ||||
|  | ||||
| 		v := el.InnerHTML(context.Background()) | ||||
| 		v := el.GetInnerHTML(context.Background()) | ||||
|  | ||||
| 		So(v, ShouldEqual, "<h2>Ferret</h2>") | ||||
| 	}) | ||||
| @@ -396,7 +396,7 @@ func TestElement(t *testing.T) { | ||||
|  | ||||
| 		So(found, ShouldNotEqual, values.None) | ||||
|  | ||||
| 		v := found.(drivers.HTMLNode).NodeName() | ||||
| 		v := found.(drivers.HTMLNode).GetNodeName() | ||||
|  | ||||
| 		So(err, ShouldBeNil) | ||||
|  | ||||
|   | ||||
							
								
								
									
										199
									
								
								pkg/drivers/http/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								pkg/drivers/http/page.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -7,6 +7,7 @@ var ( | ||||
| 	HTTPCookieType   = core.NewType("HTTPCookie") | ||||
| 	HTMLElementType  = core.NewType("HTMLElement") | ||||
| 	HTMLDocumentType = core.NewType("HTMLDocument") | ||||
| 	HTMLPageType     = core.NewType("HTMLPageType") | ||||
| ) | ||||
|  | ||||
| // Comparison table of builtin types | ||||
| @@ -15,6 +16,7 @@ var typeComparisonTable = map[core.Type]uint64{ | ||||
| 	HTTPCookieType:   1, | ||||
| 	HTMLElementType:  2, | ||||
| 	HTMLDocumentType: 3, | ||||
| 	HTMLPageType:     4, | ||||
| } | ||||
|  | ||||
| func Compare(first, second core.Type) int64 { | ||||
|   | ||||
| @@ -24,9 +24,11 @@ type ( | ||||
| 		collections.Measurable | ||||
| 		io.Closer | ||||
|  | ||||
| 		NodeType() values.Int | ||||
| 		IsDetached() values.Boolean | ||||
|  | ||||
| 		NodeName() values.String | ||||
| 		GetNodeType() values.Int | ||||
|  | ||||
| 		GetNodeName() values.String | ||||
|  | ||||
| 		GetChildNodes(ctx context.Context) core.Value | ||||
|  | ||||
| @@ -41,13 +43,13 @@ type ( | ||||
| 		ExistsBySelector(ctx context.Context, selector values.String) values.Boolean | ||||
| 	} | ||||
|  | ||||
| 	// 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 { | ||||
| 		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 | ||||
|  | ||||
| @@ -98,28 +100,20 @@ type ( | ||||
| 		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 { | ||||
| 		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 | ||||
|  | ||||
| 		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) | ||||
| 		GetChildDocuments(ctx context.Context) (*values.Array, error) | ||||
|  | ||||
| 		ClickBySelector(ctx context.Context, selector values.String) (values.Boolean, error) | ||||
|  | ||||
| @@ -129,10 +123,6 @@ type ( | ||||
|  | ||||
| 		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 | ||||
|  | ||||
| 		ScrollBottom(ctx context.Context) error | ||||
| @@ -145,8 +135,6 @@ type ( | ||||
|  | ||||
| 		MoveMouseBySelector(ctx context.Context, selector values.String) error | ||||
|  | ||||
| 		WaitForNavigation(ctx context.Context) 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 | ||||
| @@ -161,6 +149,45 @@ type ( | ||||
|  | ||||
| 		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 ( | ||||
|   | ||||
| @@ -120,6 +120,7 @@ func TestArrayIterator(t *testing.T) { | ||||
|  | ||||
| 		So(item, ShouldBeNil) | ||||
| 		So(err, ShouldBeNil) | ||||
| 		So(res, ShouldHaveLength, int(arr.Length())) | ||||
| 	}) | ||||
|  | ||||
| 	Convey("Should NOT iterate over an empty array", t, func() { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| // Type represents runtime type with id for quick type check | ||||
| // and Name for error messages | ||||
| // and GetName for error messages | ||||
|  | ||||
| //revive:disable-next-line:redefines-builtin-id | ||||
| type ( | ||||
|   | ||||
| @@ -246,6 +246,7 @@ func TestAdd(t *testing.T) { | ||||
| 			} | ||||
|  | ||||
| 			for _, argN := range args { | ||||
| 				argN := argN | ||||
| 				Convey(argN.Type().String(), func() { | ||||
| 					So(operators.Add(arg1, argN), ShouldEqual, values.NewInt(1)) | ||||
| 				}) | ||||
| @@ -743,6 +744,7 @@ func TestMultiply(t *testing.T) { | ||||
| 			} | ||||
|  | ||||
| 			for _, argN := range args { | ||||
| 				argN := argN | ||||
| 				Convey(argN.Type().String(), func() { | ||||
| 					So(operators.Multiply(arg1, argN), ShouldEqual, values.NewInt(0)) | ||||
| 				}) | ||||
| @@ -1011,6 +1013,7 @@ func TestDivide(t *testing.T) { | ||||
| 			} | ||||
|  | ||||
| 			for _, argN := range args { | ||||
| 				argN := argN | ||||
| 				Convey(argN.Type().String(), func() { | ||||
| 					So(func() { | ||||
| 						operators.Divide(arg1, argN) | ||||
|   | ||||
| @@ -34,15 +34,7 @@ func ParseBoolean(input interface{}) (Boolean, error) { | ||||
| 	s, ok := input.(string) | ||||
|  | ||||
| 	if ok { | ||||
| 		s := strings.ToLower(s) | ||||
|  | ||||
| 		if s == "true" { | ||||
| 			return True, nil | ||||
| 		} | ||||
|  | ||||
| 		if s == "false" { | ||||
| 			return False, nil | ||||
| 		} | ||||
| 		return Boolean(strings.ToLower(s) == "true"), nil | ||||
| 	} | ||||
|  | ||||
| 	return False, core.Error(core.ErrInvalidType, "expected 'bool'") | ||||
|   | ||||
| @@ -40,7 +40,7 @@ func (v TestValue) Copy() core.Value { | ||||
| } | ||||
|  | ||||
| func TestType(t *testing.T) { | ||||
| 	Convey(".Name", t, func() { | ||||
| 	Convey(".GetName", t, func() { | ||||
| 		So(types.None.String(), ShouldEqual, "none") | ||||
| 		So(types.Boolean.String(), ShouldEqual, "boolean") | ||||
| 		So(types.Int.String(), ShouldEqual, "int") | ||||
|   | ||||
| @@ -23,6 +23,7 @@ func Minus(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	capacity := values.NewInt(0) | ||||
|  | ||||
| 	for idx, i := range args { | ||||
| 		idx := idx | ||||
| 		err := core.ValidateType(i, types.Array) | ||||
|  | ||||
| 		if err != nil { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
| @@ -18,7 +19,7 @@ func AttributeGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| @@ -18,7 +19,7 @@ func AttributeRemove(ctx context.Context, args ...core.Value) (core.Value, error | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| @@ -19,7 +20,7 @@ func AttributeSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -3,12 +3,13 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
|  | ||||
| // Click dispatches click event on a given element | ||||
| // @param source (Document | Element) - Event source. | ||||
| // @param source (Open | GetElement) - Event source. | ||||
| // @param selector (String, optional) - Optional selector. Only used when a document instance is passed. | ||||
| func Click(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 2) | ||||
| @@ -19,7 +20,7 @@ func Click(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
|  | ||||
| 	// CLICK(el) | ||||
| 	if len(args) == 1 { | ||||
| 		el, err := toElement(args[0]) | ||||
| 		el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.False, err | ||||
| @@ -29,7 +30,7 @@ func Click(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	} | ||||
|  | ||||
| 	// CLICK(doc, selector) | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.False, err | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| // ClickAll dispatches click event on all matched element | ||||
| // @param source (Document) - Document. | ||||
| // @param source (Open) - Open. | ||||
| // @param selector (String) - Selector. | ||||
| // @returns (Boolean) - Returns true if matched at least one element. | ||||
| func ClickAll(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| @@ -19,20 +19,13 @@ func ClickAll(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	arg1 := args[0] | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	selector := args[1].String() | ||||
|  | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLDocumentType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	return doc.ClickBySelectorAll(ctx, values.NewString(selector)) | ||||
| } | ||||
|   | ||||
| @@ -2,14 +2,15 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // CookieSet gets a cookie from a given document by name. | ||||
| // @param source (HTMLDocument) - Target HTMLDocument. | ||||
| // CookieSet gets a cookie from a given page by name. | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param cookie (...HTTPCookie|String) - Cookie or cookie name to delete. | ||||
| func CookieDel(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
| @@ -18,13 +19,12 @@ func CookieDel(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
| 	page, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc := args[0].(drivers.HTMLDocument) | ||||
| 	inputs := args[1:] | ||||
| 	var currentCookies *values.Array | ||||
| 	cookies := make([]drivers.HTTPCookie, 0, len(inputs)) | ||||
| @@ -33,7 +33,7 @@ func CookieDel(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		switch cookie := c.(type) { | ||||
| 		case values.String: | ||||
| 			if currentCookies == nil { | ||||
| 				current, err := doc.GetCookies(ctx) | ||||
| 				current, err := page.GetCookies(ctx) | ||||
|  | ||||
| 				if err != nil { | ||||
| 					return values.None, err | ||||
| @@ -60,5 +60,5 @@ func CookieDel(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return values.None, doc.DeleteCookies(ctx, cookies...) | ||||
| 	return values.None, page.DeleteCookies(ctx, cookies...) | ||||
| } | ||||
|   | ||||
| @@ -9,8 +9,8 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // CookieSet gets a cookie from a given document by name. | ||||
| // @param doc (HTMLDocument) - Target HTMLDocument. | ||||
| // CookieSet gets a cookie from a given page by name. | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param name (String) - Cookie or cookie name to delete. | ||||
| func CookieGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, 2) | ||||
| @@ -19,7 +19,7 @@ func CookieGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
| 	page, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -31,10 +31,9 @@ func CookieGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc := args[0].(drivers.HTMLDocument) | ||||
| 	name := args[1].(values.String) | ||||
|  | ||||
| 	cookies, err := doc.GetCookies(ctx) | ||||
| 	cookies, err := page.GetCookies(ctx) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -8,8 +8,8 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
|  | ||||
| // CookieSet sets cookies to a given document | ||||
| // @param doc (HTMLDocument) - Target document. | ||||
| // CookieSet sets cookies to a given page | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param cookie... (HTTPCookie) - Target cookies. | ||||
| func CookieSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, core.MaxArgs) | ||||
| @@ -18,14 +18,12 @@ func CookieSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
| 	page, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc := args[0].(drivers.HTMLDocument) | ||||
|  | ||||
| 	cookies := make([]drivers.HTTPCookie, 0, len(args)-1) | ||||
|  | ||||
| 	for _, c := range args[1:] { | ||||
| @@ -38,5 +36,5 @@ func CookieSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		cookies = append(cookies, cookie) | ||||
| 	} | ||||
|  | ||||
| 	return values.None, doc.SetCookies(ctx, cookies...) | ||||
| 	return values.None, page.SetCookies(ctx, cookies...) | ||||
| } | ||||
|   | ||||
| @@ -12,22 +12,22 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| type DocumentLoadParams struct { | ||||
| 	drivers.LoadDocumentParams | ||||
| type PageLoadParams struct { | ||||
| 	drivers.OpenPageParams | ||||
| 	Driver  string | ||||
| 	Timeout time.Duration | ||||
| } | ||||
|  | ||||
| // Document loads a HTML document by a given url. | ||||
| // Open opens an HTML page by a given url. | ||||
| // By default, loads a document by http call - resulted document does not support any interactions. | ||||
| // If passed "true" as a second argument, headless browser is used for loading the document which support interactions. | ||||
| // @param url (String) - Target url string. If passed "about:blank" for dynamic document - it will open an empty page. | ||||
| // @param isDynamicOrParams (Boolean|DocumentLoadParams) - Either a boolean value that indicates whether to use dynamic page | ||||
| // @param isDynamicOrParams (Boolean|PageLoadParams) - Either a boolean value that indicates whether to use dynamic page | ||||
| // or an object with the following properties : | ||||
| // 		dynamic (Boolean) - Optional, indicates whether to use dynamic page. | ||||
| // 		timeout (Int) - Optional, Document load timeout. | ||||
| // 		timeout (Int) - Optional, Open load timeout. | ||||
| // @returns (HTMLDocument) - Returns loaded HTML document. | ||||
| func Document(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| func Open(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 2) | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -42,12 +42,12 @@ func Document(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
|  | ||||
| 	url := args[0].(values.String) | ||||
|  | ||||
| 	var params DocumentLoadParams | ||||
| 	var params PageLoadParams | ||||
|  | ||||
| 	if len(args) == 1 { | ||||
| 		params = newDefaultDocLoadParams(url) | ||||
| 	} else { | ||||
| 		p, err := newDocLoadParams(url, args[1]) | ||||
| 		p, err := newPageLoadParams(url, args[1]) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| @@ -65,19 +65,19 @@ func Document(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	return drv.LoadDocument(ctx, params.LoadDocumentParams) | ||||
| 	return drv.Open(ctx, params.OpenPageParams) | ||||
| } | ||||
|  | ||||
| func newDefaultDocLoadParams(url values.String) DocumentLoadParams { | ||||
| 	return DocumentLoadParams{ | ||||
| 		LoadDocumentParams: drivers.LoadDocumentParams{ | ||||
| func newDefaultDocLoadParams(url values.String) PageLoadParams { | ||||
| 	return PageLoadParams{ | ||||
| 		OpenPageParams: drivers.OpenPageParams{ | ||||
| 			URL: url.String(), | ||||
| 		}, | ||||
| 		Timeout: time.Second * 30, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func newDocLoadParams(url values.String, arg core.Value) (DocumentLoadParams, error) { | ||||
| func newPageLoadParams(url values.String, arg core.Value) (PageLoadParams, error) { | ||||
| 	res := newDefaultDocLoadParams(url) | ||||
|  | ||||
| 	if err := core.ValidateType(arg, types.Boolean, types.String, types.Object); err != nil { | ||||
|   | ||||
| @@ -10,8 +10,8 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // Download a resource from the given URL. | ||||
| // @param URL (String) - URL to download. | ||||
| // Download a resource from the given GetURL. | ||||
| // @param GetURL (String) - GetURL to download. | ||||
| // @returns data (Binary) - Returns a base64 encoded string in binary format. | ||||
| func Download(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 1) | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // Element finds an element by a given CSS selector. | ||||
| // GetElement finds an element by a given CSS selector. | ||||
| // Returns NONE if element not found. | ||||
| // @param docOrEl (HTMLDocument|HTMLElement) - Parent document or element. | ||||
| // @param selector (String) - CSS selector. | ||||
| @@ -24,14 +24,14 @@ func Element(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	return el.QuerySelector(ctx, selector), nil | ||||
| } | ||||
|  | ||||
| func queryArgs(args []core.Value) (drivers.HTMLNode, values.String, error) { | ||||
| func queryArgs(args []core.Value) (drivers.HTMLElement, values.String, error) { | ||||
| 	err := core.ValidateArgs(args, 2, 2) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, values.EmptyString, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, values.EmptyString, err | ||||
| @@ -43,5 +43,5 @@ func queryArgs(args []core.Value) (drivers.HTMLNode, values.String, error) { | ||||
| 		return nil, values.EmptyString, err | ||||
| 	} | ||||
|  | ||||
| 	return args[0].(drivers.HTMLNode), args[1].(values.String), nil | ||||
| 	return el, args[1].(values.String), nil | ||||
| } | ||||
|   | ||||
| @@ -20,8 +20,8 @@ func Hover(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	// document or element | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| 	// page or document or element | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLPageType, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -40,6 +40,12 @@ func Hover(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	} | ||||
|  | ||||
| 	switch n := args[0].(type) { | ||||
| 	case drivers.HTMLPage: | ||||
| 		if selector == values.EmptyString { | ||||
| 			return values.None, core.Error(core.ErrMissedArgument, "selector") | ||||
| 		} | ||||
|  | ||||
| 		return values.None, n.GetMainFrame().MoveMouseBySelector(ctx, selector) | ||||
| 	case drivers.HTMLDocument: | ||||
| 		if selector == values.EmptyString { | ||||
| 			return values.None, core.Error(core.ErrMissedArgument, "selector") | ||||
|   | ||||
| @@ -9,8 +9,8 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // InnerHTML Returns inner HTML string of a given or matched by CSS selector element | ||||
| // @param doc (Document|Element) - Parent document or element. | ||||
| // GetInnerHTML Returns inner HTML string of a given or matched by CSS selector element | ||||
| // @param doc (Open|GetElement) - Parent document or element. | ||||
| // @param selector (String, optional) - String of CSS selector. | ||||
| // @returns (String) - Inner HTML string if an element found, otherwise empty string. | ||||
| func InnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| @@ -20,20 +20,14 @@ func InnerHTML(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.EmptyString, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	if len(args) == 1 { | ||||
| 		return el.InnerHTML(ctx), nil | ||||
| 		return el.GetInnerHTML(ctx), nil | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[1], types.String) | ||||
|   | ||||
| @@ -20,19 +20,13 @@ func InnerHTMLAll(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[1], types.String) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -3,12 +3,13 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // InnerText returns inner text string of a given or matched by CSS selector element | ||||
| // GetInnerText returns inner text string of a given or matched by CSS selector element | ||||
| // @param doc (HTMLDocument|HTMLElement) - Parent document or element. | ||||
| // @param selector (String, optional) - String of CSS selector. | ||||
| // @returns (String) - Inner text if an element found, otherwise empty string. | ||||
| @@ -19,14 +20,14 @@ func InnerText(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.EmptyString, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	if len(args) == 1 { | ||||
| 		return el.InnerText(ctx), nil | ||||
| 		return el.GetInnerText(ctx), nil | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[1], types.String) | ||||
|   | ||||
| @@ -20,19 +20,13 @@ func InnerTextAll(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[1], types.String) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| // Input types a value to an underlying input element. | ||||
| // @param source (Document | Element) - Event target. | ||||
| // @param source (Open | GetElement) - Event target. | ||||
| // @param valueOrSelector (String) - Selector or a value. | ||||
| // @param value (String) - Target value. | ||||
| // @param delay (Int, optional) - Waits delay milliseconds between keystrokes | ||||
| @@ -23,14 +23,18 @@ func Input(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	} | ||||
|  | ||||
| 	arg1 := args[0] | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLPageType, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	if arg1.Type() == drivers.HTMLDocumentType { | ||||
| 		doc := arg1.(drivers.HTMLDocument) | ||||
| 	if arg1.Type() == drivers.HTMLPageType || arg1.Type() == drivers.HTMLDocumentType { | ||||
| 		doc, err := drivers.ToDocument(arg1) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.False, err | ||||
| 		} | ||||
|  | ||||
| 		// selector | ||||
| 		arg2 := args[1] | ||||
| @@ -57,7 +61,12 @@ func Input(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return doc.InputBySelector(ctx, arg2.(values.String), args[2], delay) | ||||
| 	} | ||||
|  | ||||
| 	el := arg1.(drivers.HTMLElement) | ||||
| 	el, err := drivers.ToElement(arg1) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	delay := values.Int(0) | ||||
|  | ||||
| 	if len(args) == 3 { | ||||
|   | ||||
| @@ -22,7 +22,7 @@ func NewLib() map[string]core.Function { | ||||
| 		"COOKIE_SET":        CookieSet, | ||||
| 		"CLICK":             Click, | ||||
| 		"CLICK_ALL":         ClickAll, | ||||
| 		"DOCUMENT":          Document, | ||||
| 		"DOCUMENT":          Open, | ||||
| 		"DOWNLOAD":          Download, | ||||
| 		"ELEMENT":           Element, | ||||
| 		"ELEMENT_EXISTS":    ElementExists, | ||||
| @@ -67,27 +67,29 @@ func NewLib() map[string]core.Function { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ValidateDocument(ctx context.Context, value core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateType(value, drivers.HTMLDocumentType, types.String) | ||||
| func OpenOrCastPage(ctx context.Context, value core.Value) (drivers.HTMLPage, bool, error) { | ||||
| 	err := core.ValidateType(value, drivers.HTMLPageType, types.String) | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 		return nil, false, err | ||||
| 	} | ||||
|  | ||||
| 	var doc drivers.HTMLDocument | ||||
| 	var page drivers.HTMLPage | ||||
| 	var closeAfter bool | ||||
|  | ||||
| 	if value.Type() == types.String { | ||||
| 		buf, err := Document(ctx, value, values.NewBoolean(true)) | ||||
| 		buf, err := Open(ctx, value, values.NewBoolean(true)) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| 			return nil, false, err | ||||
| 		} | ||||
|  | ||||
| 		doc = buf.(drivers.HTMLDocument) | ||||
| 		page = buf.(drivers.HTMLPage) | ||||
| 		closeAfter = true | ||||
| 	} else { | ||||
| 		doc = value.(drivers.HTMLDocument) | ||||
| 		page = value.(drivers.HTMLPage) | ||||
| 	} | ||||
|  | ||||
| 	return doc, nil | ||||
| 	return page, closeAfter, nil | ||||
| } | ||||
|  | ||||
| func waitTimeout(ctx context.Context, value values.Int) (context.Context, context.CancelFunc) { | ||||
| @@ -96,35 +98,3 @@ func waitTimeout(ctx context.Context, value values.Int) (context.Context, contex | ||||
| 		time.Duration(value)*time.Millisecond, | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func resolveElement(value core.Value) (drivers.HTMLElement, error) { | ||||
| 	vt := value.Type() | ||||
|  | ||||
| 	if vt == drivers.HTMLDocumentType { | ||||
| 		return value.(drivers.HTMLDocument).DocumentElement(), nil | ||||
| 	} else if vt == drivers.HTMLElementType { | ||||
| 		return value.(drivers.HTMLElement), nil | ||||
| 	} | ||||
|  | ||||
| 	return nil, core.TypeError(value.Type(), drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| } | ||||
|  | ||||
| func toDocument(value core.Value) (drivers.HTMLDocument, error) { | ||||
| 	err := core.ValidateType(value, drivers.HTMLDocumentType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return value.(drivers.HTMLDocument), nil | ||||
| } | ||||
|  | ||||
| func toElement(value core.Value) (drivers.HTMLElement, error) { | ||||
| 	err := core.ValidateType(value, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return value.(drivers.HTMLElement), nil | ||||
| } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ func MouseMoveXY(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -41,7 +41,5 @@ func MouseMoveXY(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	x := values.ToFloat(args[0]) | ||||
| 	y := values.ToFloat(args[1]) | ||||
|  | ||||
| 	doc := args[0].(drivers.HTMLDocument) | ||||
|  | ||||
| 	return values.None, doc.MoveMouseByXY(ctx, x, y) | ||||
| } | ||||
|   | ||||
| @@ -2,15 +2,17 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // Navigate navigates a document to a new resource. | ||||
| // Navigate navigates a given page to a new resource. | ||||
| // The operation blocks the execution until the page gets loaded. | ||||
| // Which means there is no need in WAIT_NAVIGATION function. | ||||
| // @param doc (Document) - Target document. | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param url (String) - Target url to navigate. | ||||
| // @param timeout (Int, optional) - Optional timeout. Default is 5000. | ||||
| func Navigate(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| @@ -20,7 +22,7 @@ func Navigate(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	page, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -47,5 +49,5 @@ func Navigate(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	ctx, fn := waitTimeout(ctx, timeout) | ||||
| 	defer fn() | ||||
|  | ||||
| 	return values.None, doc.Navigate(ctx, args[1].(values.String)) | ||||
| 	return values.None, page.Navigate(ctx, args[1].(values.String)) | ||||
| } | ||||
|   | ||||
| @@ -2,15 +2,17 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // NavigateBack navigates a document back within its navigation history. | ||||
| // NavigateBack navigates a given page back within its navigation history. | ||||
| // The operation blocks the execution until the page gets loaded. | ||||
| // If the history is empty, the function returns FALSE. | ||||
| // @param doc (Document) - Target document. | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param entry (Int, optional) - Optional value indicating how many pages to skip. Default 1. | ||||
| // @param timeout (Int, optional) - Optional timeout. Default is 5000. | ||||
| // @returns (Boolean) - Returns TRUE if history exists and the operation succeeded, otherwise FALSE. | ||||
| @@ -21,7 +23,7 @@ func NavigateBack(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	page, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -53,5 +55,5 @@ func NavigateBack(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	ctx, fn := waitTimeout(ctx, timeout) | ||||
| 	defer fn() | ||||
|  | ||||
| 	return doc.NavigateBack(ctx, skip) | ||||
| 	return page.NavigateBack(ctx, skip) | ||||
| } | ||||
|   | ||||
| @@ -2,15 +2,17 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // NavigateForward navigates a document forward within its navigation history. | ||||
| // NavigateForward navigates a given page forward within its navigation history. | ||||
| // The operation blocks the execution until the page gets loaded. | ||||
| // If the history is empty, the function returns FALSE. | ||||
| // @param doc (Document) - Target document. | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param entry (Int, optional) - Optional value indicating how many pages to skip. Default 1. | ||||
| // @param timeout (Int, optional) - Optional timeout. Default is 5000. | ||||
| // @returns (Boolean) - Returns TRUE if history exists and the operation succeeded, otherwise FALSE. | ||||
| @@ -21,7 +23,7 @@ func NavigateForward(ctx context.Context, args ...core.Value) (core.Value, error | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	page, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -53,5 +55,5 @@ func NavigateForward(ctx context.Context, args ...core.Value) (core.Value, error | ||||
| 	ctx, fn := waitTimeout(ctx, timeout) | ||||
| 	defer fn() | ||||
|  | ||||
| 	return doc.NavigateForward(ctx, skip) | ||||
| 	return page.NavigateForward(ctx, skip) | ||||
| } | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import ( | ||||
| // Pagination creates an iterator that goes through pages using CSS selector. | ||||
| // The iterator starts from the current page i.e. it does not change the page on 1st iteration. | ||||
| // That allows you to keep scraping logic inside FOR loop. | ||||
| // @param doc (Document) - Target document. | ||||
| // @param doc (Open) - Target document. | ||||
| // @param selector (String) - CSS selector for a pagination on the page. | ||||
| func Pagination(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 2, 2) | ||||
| @@ -21,7 +21,7 @@ func Pagination(_ context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -22,7 +22,7 @@ func ValidatePageRanges(pageRanges string) (bool, error) { | ||||
| } | ||||
|  | ||||
| // PDF print a PDF of the current page. | ||||
| // @param source (Document) - Document. | ||||
| // @param target (HTMLPage|String) - Target page or url. | ||||
| // @param params (Object) - Optional, An object containing the following properties : | ||||
| //   Landscape (Bool) - Paper orientation. Defaults to false. | ||||
| //   DisplayHeaderFooter (Bool) - Display header and footer. Defaults to false. | ||||
| @@ -48,14 +48,17 @@ func PDF(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	} | ||||
|  | ||||
| 	arg1 := args[0] | ||||
| 	val, err := ValidateDocument(ctx, arg1) | ||||
| 	page, closeAfter, err := OpenOrCastPage(ctx, arg1) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc := val.(drivers.HTMLDocument) | ||||
| 	defer doc.Close() | ||||
| 	defer func() { | ||||
| 		if closeAfter { | ||||
| 			page.Close() | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	pdfParams := drivers.PDFParams{} | ||||
|  | ||||
| @@ -292,7 +295,7 @@ func PDF(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	pdf, err := doc.PrintToPDF(ctx, pdfParams) | ||||
| 	pdf, err := page.PrintToPDF(ctx, pdfParams) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -10,8 +10,8 @@ import ( | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // Screenshot takes a screenshot of the current page. | ||||
| // @param source (Document) - Document. | ||||
| // Screenshot takes a screenshot of a given page. | ||||
| // @param target (HTMLPage|String) - Target page or url. | ||||
| // @param params (Object) - Optional, An object containing the following properties : | ||||
| // 		x (Float|Int) - Optional, X position of the viewport. | ||||
| // 		x (Float|Int) - Optional,Y position of the viewport. | ||||
| @@ -35,15 +35,17 @@ func Screenshot(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	val, err := ValidateDocument(ctx, arg1) | ||||
| 	page, closeAfter, err := OpenOrCastPage(ctx, arg1) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc := val.(drivers.HTMLDocument) | ||||
|  | ||||
| 	defer doc.Close() | ||||
| 	defer func() { | ||||
| 		if closeAfter { | ||||
| 			page.Close() | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	screenshotParams := drivers.ScreenshotParams{ | ||||
| 		X:       0, | ||||
| @@ -155,7 +157,7 @@ func Screenshot(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	scr, err := doc.CaptureScreenshot(ctx, screenshotParams) | ||||
| 	scr, err := page.CaptureScreenshot(ctx, screenshotParams) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -17,13 +17,7 @@ func ScrollBottom(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -20,7 +20,7 @@ func ScrollInto(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	} | ||||
|  | ||||
| 	if len(args) == 2 { | ||||
| 		err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
| 		doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| @@ -32,8 +32,6 @@ func ScrollInto(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		// Document with a selector | ||||
| 		doc := args[0].(drivers.HTMLDocument) | ||||
| 		selector := args[1].(values.String) | ||||
|  | ||||
| 		return values.None, doc.ScrollBySelector(ctx, selector) | ||||
| @@ -45,8 +43,12 @@ func ScrollInto(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	// Element | ||||
| 	el := args[0].(drivers.HTMLElement) | ||||
| 	// GetElement | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	return values.None, el.ScrollIntoView(ctx) | ||||
| } | ||||
|   | ||||
| @@ -17,13 +17,7 @@ func ScrollTop(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -20,7 +20,7 @@ func ScrollXY(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	err = core.ValidateType(args[0], drivers.HTMLDocumentType) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -41,7 +41,5 @@ func ScrollXY(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	x := values.ToFloat(args[1]) | ||||
| 	y := values.ToFloat(args[2]) | ||||
|  | ||||
| 	doc := args[0].(drivers.HTMLDocument) | ||||
|  | ||||
| 	return values.None, doc.ScrollByXY(ctx, x, y) | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| // Select selects a value from an underlying select element. | ||||
| // @param source (Document | Element) - Event target. | ||||
| // @param source (Open | GetElement) - Event target. | ||||
| // @param valueOrSelector (String | Array<String>) - Selector or a an array of strings as a value. | ||||
| // @param value (Array<String) - Target value. Optional. | ||||
| // @returns (Array<String>) - Returns an array of selected values. | ||||
| @@ -22,14 +22,18 @@ func Select(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	} | ||||
|  | ||||
| 	arg1 := args[0] | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLPageType, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.False, err | ||||
| 	} | ||||
|  | ||||
| 	if arg1.Type() == drivers.HTMLDocumentType { | ||||
| 		doc := arg1.(drivers.HTMLDocument) | ||||
| 	if arg1.Type() == drivers.HTMLPageType || arg1.Type() == drivers.HTMLDocumentType { | ||||
| 		doc, err := drivers.ToDocument(arg1) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		// selector | ||||
| 		arg2 := args[1] | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| ) | ||||
| @@ -18,7 +19,7 @@ func StyleGet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| @@ -18,7 +19,7 @@ func StyleRemove(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package html | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| @@ -19,7 +20,7 @@ func StyleSet(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	el, err := resolveElement(args[0]) | ||||
| 	el, err := drivers.ToElement(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -26,7 +26,7 @@ func waitAttributeWhen(ctx context.Context, args []core.Value, when drivers.Wait | ||||
|  | ||||
| 	// document or element | ||||
| 	arg1 := args[0] | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLPageType, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -43,7 +43,7 @@ func waitAttributeWhen(ctx context.Context, args []core.Value, when drivers.Wait | ||||
|  | ||||
| 	// if a document is passed | ||||
| 	// WAIT_ATTR(doc, selector, attrName, attrValue, timeout) | ||||
| 	if arg1.Type() == drivers.HTMLDocumentType { | ||||
| 	if arg1.Type() == drivers.HTMLPageType || arg1.Type() == drivers.HTMLDocumentType { | ||||
| 		// revalidate args with more accurate amount | ||||
| 		err := core.ValidateArgs(args, 4, 5) | ||||
|  | ||||
| @@ -58,7 +58,12 @@ func waitAttributeWhen(ctx context.Context, args []core.Value, when drivers.Wait | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		doc := arg1.(drivers.HTMLDocument) | ||||
| 		doc, err := drivers.ToDocument(arg1) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		selector := args[1].(values.String) | ||||
| 		name := args[2].(values.String) | ||||
| 		value := args[3] | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| @@ -35,7 +36,7 @@ func waitAttributeAllWhen(ctx context.Context, args []core.Value, when drivers.W | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| @@ -43,7 +44,7 @@ func waitClassWhen(ctx context.Context, args []core.Value, when drivers.WaitEven | ||||
|  | ||||
| 	// document or element | ||||
| 	arg1 := args[0] | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
| 	err = core.ValidateType(arg1, drivers.HTMLPageType, drivers.HTMLDocumentType, drivers.HTMLElementType) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
| @@ -59,7 +60,7 @@ func waitClassWhen(ctx context.Context, args []core.Value, when drivers.WaitEven | ||||
| 	timeout := values.NewInt(defaultTimeout) | ||||
|  | ||||
| 	// if a document is passed | ||||
| 	if arg1.Type() == drivers.HTMLDocumentType { | ||||
| 	if arg1.Type() == drivers.HTMLPageType || arg1.Type() == drivers.HTMLDocumentType { | ||||
| 		// revalidate args with more accurate amount | ||||
| 		err := core.ValidateArgs(args, 3, 4) | ||||
|  | ||||
| @@ -74,7 +75,12 @@ func waitClassWhen(ctx context.Context, args []core.Value, when drivers.WaitEven | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		doc := arg1.(drivers.HTMLDocument) | ||||
| 		doc, err := drivers.ToDocument(arg1) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return values.None, err | ||||
| 		} | ||||
|  | ||||
| 		selector := args[1].(values.String) | ||||
| 		class := args[2].(values.String) | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| @@ -35,7 +36,7 @@ func waitClassAllWhen(ctx context.Context, args []core.Value, when drivers.WaitE | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| @@ -33,7 +34,7 @@ func waitElementWhen(ctx context.Context, args []core.Value, when drivers.WaitEv | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToDocument(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
| @@ -2,14 +2,16 @@ package html | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/MontFerret/ferret/pkg/drivers" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/core" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values" | ||||
| 	"github.com/MontFerret/ferret/pkg/runtime/values/types" | ||||
| ) | ||||
|  | ||||
| // WaitNavigation waits for document to navigate to a new url. | ||||
| // WaitNavigation waits for a given page to navigate to a new url. | ||||
| // Stops the execution until the navigation ends or operation times out. | ||||
| // @param doc (HTMLDocument) - Driver HTMLDocument. | ||||
| // @param page (HTMLPage) - Target page. | ||||
| // @param timeout (Int, optional) - Optional timeout. Default 5000 ms. | ||||
| func WaitNavigation(ctx context.Context, args ...core.Value) (core.Value, error) { | ||||
| 	err := core.ValidateArgs(args, 1, 2) | ||||
| @@ -18,7 +20,7 @@ func WaitNavigation(ctx context.Context, args ...core.Value) (core.Value, error) | ||||
| 		return values.None, err | ||||
| 	} | ||||
|  | ||||
| 	doc, err := toDocument(args[0]) | ||||
| 	doc, err := drivers.ToPage(args[0]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return values.None, err | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user