mirror of
https://github.com/MontFerret/ferret.git
synced 2025-07-17 01:32:22 +02:00
Feature/425 iframe navigation (#535)
* Updated navigation logic * Fixed goroutine deadlock * Fixed closing chan * Added support of waiting for individual frame navigation * Updated EventLoop API in order to avoid double closing of event sources * Fixed attr retrieval * Removed redundant println * Updated DOM Readiness check
This commit is contained in:
80
.travis.yml
80
.travis.yml
@ -1,80 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
sudo: required
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
|
|
||||||
go:
|
|
||||||
- "1.13.x"
|
|
||||||
- stable
|
|
||||||
|
|
||||||
env:
|
|
||||||
- ANTLR_VERSION=4.8
|
|
||||||
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- openjdk-9-jre-headless
|
|
||||||
|
|
||||||
install:
|
|
||||||
- go get github.com/mgechev/revive
|
|
||||||
- sudo curl -o /usr/local/lib/antlr-${ANTLR_VERSION}-complete.jar https://www.antlr.org/download/antlr-${ANTLR_VERSION}-complete.jar
|
|
||||||
- export CLASSPATH=".:/usr/local/lib/antlr-${ANTLR_VERSION}-complete.jar:$CLASSPATH"
|
|
||||||
- mkdir $HOME/travis-bin
|
|
||||||
- echo -e '#!/bin/bash\njava -jar /usr/local/lib/antlr-${ANTLR_VERSION}-complete.jar "$@"' > $HOME/travis-bin/antlr
|
|
||||||
- echo -e '#!/bin/bash\njava org.antlr.v4.gui.TestRig "$@"' > $HOME/travis-bin/grun
|
|
||||||
- chmod +x $HOME/travis-bin/*
|
|
||||||
- export PATH=$PATH:$HOME/travis-bin
|
|
||||||
- export GO111MODULE=on
|
|
||||||
- git reset --hard
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- install
|
|
||||||
- lint
|
|
||||||
- compile
|
|
||||||
- test
|
|
||||||
- e2e
|
|
||||||
- bench
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- stage: install
|
|
||||||
go: stable
|
|
||||||
script:
|
|
||||||
- make install
|
|
||||||
- stage: lint
|
|
||||||
go: stable
|
|
||||||
script:
|
|
||||||
- make vet
|
|
||||||
- make lint
|
|
||||||
- make fmt
|
|
||||||
- git diff
|
|
||||||
- if [[ $(git diff) != '' ]]; then echo 'Invalid formatting!' >&2; exit 1; fi
|
|
||||||
- stage: compile
|
|
||||||
go: stable
|
|
||||||
script:
|
|
||||||
- make generate
|
|
||||||
- make compile
|
|
||||||
- stage: test
|
|
||||||
script:
|
|
||||||
- make cover
|
|
||||||
- stage: e2e
|
|
||||||
go: stable
|
|
||||||
before_script:
|
|
||||||
- curl https://raw.githubusercontent.com/MontFerret/lab/master/install.sh -o install.sh
|
|
||||||
- sudo sh install.sh
|
|
||||||
- docker run -d -p 9222:9222 -e CHROME_OPTS='--disable-dev-shm-usage --full-memory-crash-report' alpeware/chrome-headless-stable:ver-83.0.4103.61
|
|
||||||
- docker ps
|
|
||||||
script:
|
|
||||||
- make compile
|
|
||||||
- make e2e
|
|
||||||
after_script:
|
|
||||||
- docker stop $(docker ps -q)
|
|
||||||
- stage: bench
|
|
||||||
go: stable
|
|
||||||
script:
|
|
||||||
- make bench
|
|
4
Makefile
4
Makefile
@ -18,7 +18,7 @@ install:
|
|||||||
go get
|
go get
|
||||||
|
|
||||||
compile:
|
compile:
|
||||||
go build -v -o ${DIR_BIN}/ferret \
|
go build -race -v -o ${DIR_BIN}/ferret \
|
||||||
-ldflags "-X main.version=${VERSION}" \
|
-ldflags "-X main.version=${VERSION}" \
|
||||||
./main.go
|
./main.go
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ cover:
|
|||||||
curl -s https://codecov.io/bash | bash
|
curl -s https://codecov.io/bash | bash
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
lab --timeout=120 --concurrency=2 --wait=http://127.0.0.1:9222/json/version --runtime=bin://./bin/ferret --files=file://./e2e/tests --dir=./e2e/pages/dynamic:8080@dynamic --dir=./e2e/pages/static:8081@static
|
lab --timeout=120 --times=5 --concurrency=1 --wait=http://127.0.0.1:9222/json/version --runtime=bin://./bin/ferret --files=file://./e2e/tests --cdn=./e2e/pages/dynamic:8080@dynamic --cdn=./e2e/pages/static:8081@static
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
go test -run=XXX -bench=. ${DIR_PKG}/...
|
go test -run=XXX -bench=. ${DIR_PKG}/...
|
||||||
|
@ -41,6 +41,7 @@ func Exec(query string, opts Options) {
|
|||||||
|
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(c, os.Interrupt)
|
||||||
|
signal.Notify(c, os.Kill)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
@ -3,6 +3,24 @@ import { parse } from '../../../utils/qs.js';
|
|||||||
const e = React.createElement;
|
const e = React.createElement;
|
||||||
|
|
||||||
export default class IFramePage extends React.Component {
|
export default class IFramePage extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
url: '/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUrlInput(evt) {
|
||||||
|
this.setState({
|
||||||
|
url: evt.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReload() {
|
||||||
|
window.location.href = this.state.url;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const search = parse(this.props.location.search);
|
const search = parse(this.props.location.search);
|
||||||
|
|
||||||
@ -12,7 +30,30 @@ export default class IFramePage extends React.Component {
|
|||||||
redirect = search.src;
|
redirect = search.src;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let navGroup;
|
||||||
|
|
||||||
|
if (window.top !== window) {
|
||||||
|
navGroup = [
|
||||||
|
e("div", { className: "form-group row" }, [
|
||||||
|
e("input", {
|
||||||
|
id: "url_input",
|
||||||
|
type: "text",
|
||||||
|
className: "form-control",
|
||||||
|
onChange: this.handleUrlInput.bind(this)
|
||||||
|
}),
|
||||||
|
e("button", {
|
||||||
|
id: "submit",
|
||||||
|
className: "btn btn-primary",
|
||||||
|
onClick: this.handleReload.bind(this)
|
||||||
|
}, [
|
||||||
|
"Navigate"
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return e("div", { id: "iframe" }, [
|
return e("div", { id: "iframe" }, [
|
||||||
|
navGroup,
|
||||||
e("iframe", {
|
e("iframe", {
|
||||||
name: 'nested',
|
name: 'nested',
|
||||||
style: {
|
style: {
|
||||||
|
8
e2e/tests/dynamic/doc/click/click.fql
Normal file
8
e2e/tests/dynamic/doc/click/click.fql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
LET url = @lab.cdn.dynamic + "/#/events"
|
||||||
|
LET page = DOCUMENT(url, { driver: "cdp" })
|
||||||
|
|
||||||
|
CLICK(page, "#wait-class-random-btn")
|
||||||
|
|
||||||
|
WAIT_CLASS(page, "#wait-class-random-content", "alert-success")
|
||||||
|
|
||||||
|
RETURN ""
|
8
e2e/tests/dynamic/doc/click/click_by_selector.fql
Normal file
8
e2e/tests/dynamic/doc/click/click_by_selector.fql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
LET url = @lab.cdn.dynamic + "/#/events"
|
||||||
|
LET page = DOCUMENT(url, true)
|
||||||
|
|
||||||
|
CLICK(page, "#wait-class-random button")
|
||||||
|
|
||||||
|
WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000)
|
||||||
|
|
||||||
|
RETURN ""
|
16
e2e/tests/dynamic/doc/click/click_by_selector_with_count.fql
Normal file
16
e2e/tests/dynamic/doc/click/click_by_selector_with_count.fql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
LET url = @lab.cdn.dynamic + "/#/forms"
|
||||||
|
LET page = DOCUMENT(url, true)
|
||||||
|
|
||||||
|
WAIT_ELEMENT(page, "form")
|
||||||
|
|
||||||
|
LET input = ELEMENT(page, "#text_input")
|
||||||
|
|
||||||
|
INPUT(input, "Foo")
|
||||||
|
|
||||||
|
CLICK(page, "#text_input", 2)
|
||||||
|
|
||||||
|
INPUT(input, "Bar")
|
||||||
|
|
||||||
|
WAIT(100)
|
||||||
|
|
||||||
|
RETURN T::EQ(input.value, "Bar")
|
16
e2e/tests/dynamic/doc/click/click_with_count.fql
Normal file
16
e2e/tests/dynamic/doc/click/click_with_count.fql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
LET url = @lab.cdn.dynamic + "/#/forms"
|
||||||
|
LET page = DOCUMENT(url, true)
|
||||||
|
|
||||||
|
WAIT_ELEMENT(page, "form")
|
||||||
|
|
||||||
|
LET input = ELEMENT(page, "#text_input")
|
||||||
|
|
||||||
|
INPUT(input, "Foo")
|
||||||
|
|
||||||
|
CLICK(page, "#text_input", 2)
|
||||||
|
|
||||||
|
INPUT(input, "Bar")
|
||||||
|
|
||||||
|
WAIT(100)
|
||||||
|
|
||||||
|
RETURN T::EQ(input.value, "Bar")
|
@ -1,5 +1,5 @@
|
|||||||
LET url = @lab.cdn.dynamic + "?redirect=/iframe"
|
LET url = @lab.cdn.dynamic + "?redirect=/iframe"
|
||||||
LET page = DOCUMENT(url, { driver: 'cdp' })
|
LET page = DOCUMENT(url, { driver: 'cdp', timeout: 5000 })
|
||||||
|
|
||||||
LET frames = (
|
LET frames = (
|
||||||
FOR frame IN page.frames
|
FOR frame IN page.frames
|
||||||
|
14
e2e/tests/dynamic/doc/wait/frame_navigation.fql
Normal file
14
e2e/tests/dynamic/doc/wait/frame_navigation.fql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/iframe"
|
||||||
|
LET page = DOCUMENT(url, { driver: 'cdp' })
|
||||||
|
LET original = FIRST(FRAMES(page, "name", "nested"))
|
||||||
|
|
||||||
|
INPUT(original, "#url_input", "https://getbootstrap.com/")
|
||||||
|
CLICK(original, "#submit")
|
||||||
|
|
||||||
|
WAIT_NAVIGATION(page, {
|
||||||
|
frame: original
|
||||||
|
})
|
||||||
|
|
||||||
|
LET current = FIRST(FRAMES(page, "name", "nested"))
|
||||||
|
|
||||||
|
RETURN T::EQ(current.URL, "https://getbootstrap.com/")
|
@ -2,6 +2,6 @@ LET origin = "https://github.com/"
|
|||||||
LET doc = DOCUMENT(origin, { driver: "cdp" })
|
LET doc = DOCUMENT(origin, { driver: "cdp" })
|
||||||
|
|
||||||
NAVIGATE(doc, "https://github.com/features", 10000)
|
NAVIGATE(doc, "https://github.com/features", 10000)
|
||||||
NAVIGATE_BACK(doc, 10000)
|
NAVIGATE_BACK(doc)
|
||||||
|
|
||||||
RETURN doc.url == origin
|
RETURN doc.url == origin
|
||||||
|
@ -2,8 +2,8 @@ LET origin = "https://github.com/"
|
|||||||
LET target = "https://github.com/features"
|
LET target = "https://github.com/features"
|
||||||
LET doc = DOCUMENT(origin, { driver: "cdp" })
|
LET doc = DOCUMENT(origin, { driver: "cdp" })
|
||||||
|
|
||||||
NAVIGATE(doc, target, 10000)
|
NAVIGATE(doc, target)
|
||||||
NAVIGATE_BACK(doc, 10000)
|
NAVIGATE_BACK(doc)
|
||||||
NAVIGATE_FORWARD(doc, 10000)
|
NAVIGATE_FORWARD(doc)
|
||||||
|
|
||||||
RETURN doc.url == target
|
RETURN doc.url == target
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
LET doc = DOCUMENT("https://github.com/", { driver: "cdp" })
|
LET doc = DOCUMENT("https://github.com/", { driver: "cdp" })
|
||||||
|
|
||||||
NAVIGATE(doc, "https://github.com/features", 10000)
|
NAVIGATE(doc, "https://github.com/features")
|
||||||
NAVIGATE(doc, "https://github.com/enterprise", 10000)
|
NAVIGATE(doc, "https://github.com/enterprise")
|
||||||
NAVIGATE(doc, "https://github.com/marketplace", 10000)
|
NAVIGATE(doc, "https://github.com/marketplace")
|
||||||
NAVIGATE_BACK(doc, 3, 10000)
|
NAVIGATE_BACK(doc, 3)
|
||||||
NAVIGATE_FORWARD(doc, 2, 10000)
|
NAVIGATE_FORWARD(doc, 2)
|
||||||
|
|
||||||
RETURN doc.url == "https://github.com/enterprise"
|
RETURN doc.url == "https://github.com/enterprise"
|
||||||
|
@ -416,7 +416,8 @@ func (el *HTMLElement) GetAttribute(ctx context.Context, name values.String) (co
|
|||||||
return values.None, err
|
return values.None, err
|
||||||
}
|
}
|
||||||
|
|
||||||
result := values.EmptyString
|
var result core.Value
|
||||||
|
result = values.None
|
||||||
targetName := strings.ToLower(name.String())
|
targetName := strings.ToLower(name.String())
|
||||||
|
|
||||||
traverseAttrs(repl.Attributes, func(name, value string) bool {
|
traverseAttrs(repl.Attributes, func(name, value string) bool {
|
||||||
|
129
pkg/drivers/cdp/dom/frame.go
Normal file
129
pkg/drivers/cdp/dom/frame.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
package dom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Frame struct {
|
||||||
|
tree page.FrameTree
|
||||||
|
node *HTMLDocument
|
||||||
|
ready bool
|
||||||
|
}
|
||||||
|
|
||||||
|
AtomicFrameID struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
value page.FrameID
|
||||||
|
}
|
||||||
|
|
||||||
|
AtomicFrameCollection struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
value map[page.FrameID]Frame
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewAtomicFrameID() *AtomicFrameID {
|
||||||
|
return &AtomicFrameID{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *AtomicFrameID) Get() page.FrameID {
|
||||||
|
id.mu.Lock()
|
||||||
|
defer id.mu.Unlock()
|
||||||
|
|
||||||
|
return id.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *AtomicFrameID) Set(value page.FrameID) {
|
||||||
|
id.mu.Lock()
|
||||||
|
defer id.mu.Unlock()
|
||||||
|
|
||||||
|
id.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *AtomicFrameID) Reset() {
|
||||||
|
id.mu.Lock()
|
||||||
|
defer id.mu.Unlock()
|
||||||
|
|
||||||
|
id.value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *AtomicFrameID) IsEmpty() bool {
|
||||||
|
id.mu.Lock()
|
||||||
|
defer id.mu.Unlock()
|
||||||
|
|
||||||
|
return id.value == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAtomicFrameCollection() *AtomicFrameCollection {
|
||||||
|
return &AtomicFrameCollection{
|
||||||
|
value: make(map[page.FrameID]Frame),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) Length() int {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
return len(fc.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) ForEach(predicate func(value Frame, key page.FrameID) bool) {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
for k, v := range fc.value {
|
||||||
|
if predicate(v, k) == false {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) Has(key page.FrameID) bool {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
_, ok := fc.value[key]
|
||||||
|
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) Get(key page.FrameID) (Frame, bool) {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
found, ok := fc.value[key]
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return found, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return Frame{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) Set(key page.FrameID, value Frame) {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
fc.value[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) Remove(key page.FrameID) {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
delete(fc.value, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fc *AtomicFrameCollection) ToSlice() []Frame {
|
||||||
|
fc.mu.Lock()
|
||||||
|
defer fc.mu.Unlock()
|
||||||
|
|
||||||
|
slice := make([]Frame, 0, len(fc.value))
|
||||||
|
|
||||||
|
for _, v := range fc.value {
|
||||||
|
slice = append(slice, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return slice
|
||||||
|
}
|
@ -2,21 +2,18 @@ package dom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
|
||||||
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
|
||||||
"github.com/MontFerret/ferret/pkg/drivers/common"
|
"github.com/MontFerret/ferret/pkg/drivers/common"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||||
|
"github.com/mafredri/cdp"
|
||||||
|
"github.com/mafredri/cdp/protocol/dom"
|
||||||
"github.com/mafredri/cdp/protocol/page"
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
|
"github.com/mafredri/cdp/rpcc"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"io"
|
"io"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp"
|
|
||||||
"github.com/mafredri/cdp/protocol/dom"
|
|
||||||
"github.com/mafredri/cdp/rpcc"
|
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -38,21 +35,14 @@ type (
|
|||||||
|
|
||||||
ChildNodeRemovedListener func(ctx context.Context, nodeID, previousNodeID dom.NodeID)
|
ChildNodeRemovedListener func(ctx context.Context, nodeID, previousNodeID dom.NodeID)
|
||||||
|
|
||||||
Frame struct {
|
|
||||||
tree page.FrameTree
|
|
||||||
node *HTMLDocument
|
|
||||||
ready bool
|
|
||||||
}
|
|
||||||
|
|
||||||
Manager struct {
|
Manager struct {
|
||||||
mu sync.Mutex
|
|
||||||
logger *zerolog.Logger
|
logger *zerolog.Logger
|
||||||
client *cdp.Client
|
client *cdp.Client
|
||||||
events *events.Loop
|
events *events.Loop
|
||||||
mouse *input.Mouse
|
mouse *input.Mouse
|
||||||
keyboard *input.Keyboard
|
keyboard *input.Keyboard
|
||||||
mainFrame page.FrameID
|
mainFrame *AtomicFrameID
|
||||||
frames map[page.FrameID]Frame
|
frames *AtomicFrameCollection
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -154,39 +144,33 @@ func New(
|
|||||||
manager.events = eventLoop
|
manager.events = eventLoop
|
||||||
manager.mouse = mouse
|
manager.mouse = mouse
|
||||||
manager.keyboard = keyboard
|
manager.keyboard = keyboard
|
||||||
manager.frames = make(map[page.FrameID]Frame)
|
manager.mainFrame = NewAtomicFrameID()
|
||||||
|
manager.frames = NewAtomicFrameCollection()
|
||||||
manager.cancel = cancel
|
manager.cancel = cancel
|
||||||
|
|
||||||
eventLoop.Start()
|
eventLoop.Run(ctx)
|
||||||
|
|
||||||
return manager, nil
|
return manager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Close() error {
|
func (m *Manager) Close() error {
|
||||||
m.mu.Lock()
|
errs := make([]error, 0, m.frames.Length()+1)
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
errs := make([]error, 0, len(m.frames)+1)
|
|
||||||
|
|
||||||
if m.cancel != nil {
|
if m.cancel != nil {
|
||||||
m.cancel()
|
m.cancel()
|
||||||
m.cancel = nil
|
m.cancel = nil
|
||||||
|
|
||||||
err := m.events.Stop().Close()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range m.frames {
|
m.frames.ForEach(func(f Frame, key page.FrameID) bool {
|
||||||
// if initialized
|
// if initialized
|
||||||
if f.node != nil {
|
if f.node != nil {
|
||||||
if err := f.node.Close(); err != nil {
|
if err := f.node.Close(); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return core.Errors(errs...)
|
return core.Errors(errs...)
|
||||||
@ -196,14 +180,13 @@ func (m *Manager) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetMainFrame() *HTMLDocument {
|
func (m *Manager) GetMainFrame() *HTMLDocument {
|
||||||
m.mu.Lock()
|
mainFrameID := m.mainFrame.Get()
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if m.mainFrame == "" {
|
if mainFrameID == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
mainFrame, exists := m.frames[m.mainFrame]
|
mainFrame, exists := m.frames.Get(mainFrameID)
|
||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
return mainFrame.node
|
return mainFrame.node
|
||||||
@ -213,46 +196,33 @@ func (m *Manager) GetMainFrame() *HTMLDocument {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) SetMainFrame(doc *HTMLDocument) {
|
func (m *Manager) SetMainFrame(doc *HTMLDocument) {
|
||||||
m.mu.Lock()
|
mainFrameID := m.mainFrame.Get()
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
if m.mainFrame != "" {
|
if mainFrameID != "" {
|
||||||
if err := m.removeFrameRecursivelyInternal(m.mainFrame); err != nil {
|
if err := m.removeFrameRecursivelyInternal(mainFrameID); err != nil {
|
||||||
m.logger.Error().Err(err).Msg("failed to close previous main frame")
|
m.logger.Error().Err(err).Msg("failed to close previous main frame")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mainFrame = doc.frameTree.Frame.ID
|
m.mainFrame.Set(doc.frameTree.Frame.ID)
|
||||||
|
|
||||||
m.addPreloadedFrame(doc)
|
m.addPreloadedFrame(doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) AddFrame(frame page.FrameTree) {
|
func (m *Manager) AddFrame(frame page.FrameTree) {
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
m.addFrameInternal(frame)
|
m.addFrameInternal(frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RemoveFrame(frameID page.FrameID) error {
|
func (m *Manager) RemoveFrame(frameID page.FrameID) error {
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
return m.removeFrameInternal(frameID)
|
return m.removeFrameInternal(frameID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RemoveFrameRecursively(frameID page.FrameID) error {
|
func (m *Manager) RemoveFrameRecursively(frameID page.FrameID) error {
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
return m.removeFrameRecursivelyInternal(frameID)
|
return m.removeFrameRecursivelyInternal(frameID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) RemoveFramesByParentID(parentFrameID page.FrameID) error {
|
func (m *Manager) RemoveFramesByParentID(parentFrameID page.FrameID) error {
|
||||||
m.mu.Lock()
|
frame, found := m.frames.Get(parentFrameID)
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
frame, found := m.frames[parentFrameID]
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return errors.New("frame not found")
|
return errors.New("frame not found")
|
||||||
@ -268,17 +238,11 @@ func (m *Manager) RemoveFramesByParentID(parentFrameID page.FrameID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetFrameNode(ctx context.Context, frameID page.FrameID) (*HTMLDocument, error) {
|
func (m *Manager) GetFrameNode(ctx context.Context, frameID page.FrameID) (*HTMLDocument, error) {
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
return m.getFrameInternal(ctx, frameID)
|
return m.getFrameInternal(ctx, frameID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetFrameTree(_ context.Context, frameID page.FrameID) (page.FrameTree, error) {
|
func (m *Manager) GetFrameTree(_ context.Context, frameID page.FrameID) (page.FrameTree, error) {
|
||||||
m.mu.Lock()
|
frame, found := m.frames.Get(frameID)
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
frame, found := m.frames[frameID]
|
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return page.FrameTree{}, core.ErrNotFound
|
return page.FrameTree{}, core.ErrNotFound
|
||||||
@ -288,12 +252,9 @@ func (m *Manager) GetFrameTree(_ context.Context, frameID page.FrameID) (page.Fr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetFrameNodes(ctx context.Context) (*values.Array, error) {
|
func (m *Manager) GetFrameNodes(ctx context.Context) (*values.Array, error) {
|
||||||
m.mu.Lock()
|
arr := values.NewArray(m.frames.Length())
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
arr := values.NewArray(len(m.frames))
|
for _, f := range m.frames.ToSlice() {
|
||||||
|
|
||||||
for _, f := range m.frames {
|
|
||||||
doc, err := m.getFrameInternal(ctx, f.tree.Frame.ID)
|
doc, err := m.getFrameInternal(ctx, f.tree.Frame.ID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -346,29 +307,11 @@ func (m *Manager) RemoveChildNodeRemovedListener(listenerID events.ListenerID) {
|
|||||||
m.events.RemoveListener(eventChildNodeRemoved, listenerID)
|
m.events.RemoveListener(eventChildNodeRemoved, listenerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) WaitForDOMReady(ctx context.Context) error {
|
|
||||||
onContentReady, err := m.client.Page.DOMContentEventFired(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := onContentReady.Close(); err != nil {
|
|
||||||
m.logger.Error().Err(err).Msg("failed to close DOM content ready stream event")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_, err = onContentReady.Recv()
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) addFrameInternal(frame page.FrameTree) {
|
func (m *Manager) addFrameInternal(frame page.FrameTree) {
|
||||||
m.frames[frame.Frame.ID] = Frame{
|
m.frames.Set(frame.Frame.ID, Frame{
|
||||||
tree: frame,
|
tree: frame,
|
||||||
node: nil,
|
node: nil,
|
||||||
}
|
})
|
||||||
|
|
||||||
for _, child := range frame.ChildFrames {
|
for _, child := range frame.ChildFrames {
|
||||||
m.addFrameInternal(child)
|
m.addFrameInternal(child)
|
||||||
@ -376,10 +319,10 @@ func (m *Manager) addFrameInternal(frame page.FrameTree) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) addPreloadedFrame(doc *HTMLDocument) {
|
func (m *Manager) addPreloadedFrame(doc *HTMLDocument) {
|
||||||
m.frames[doc.frameTree.Frame.ID] = Frame{
|
m.frames.Set(doc.frameTree.Frame.ID, Frame{
|
||||||
tree: doc.frameTree,
|
tree: doc.frameTree,
|
||||||
node: doc,
|
node: doc,
|
||||||
}
|
})
|
||||||
|
|
||||||
for _, child := range doc.frameTree.ChildFrames {
|
for _, child := range doc.frameTree.ChildFrames {
|
||||||
m.addFrameInternal(child)
|
m.addFrameInternal(child)
|
||||||
@ -387,7 +330,7 @@ func (m *Manager) addPreloadedFrame(doc *HTMLDocument) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (*HTMLDocument, error) {
|
func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (*HTMLDocument, error) {
|
||||||
frame, found := m.frames[frameID]
|
frame, found := m.frames.Get(frameID)
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
return nil, core.ErrNotFound
|
return nil, core.ErrNotFound
|
||||||
@ -402,7 +345,7 @@ func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (*
|
|||||||
node, execID, err := resolveFrame(ctx, m.client, frameID)
|
node, execID, err := resolveFrame(ctx, m.client, frameID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to resolve frame node")
|
return nil, errors.Wrapf(err, "failed to resolve frame node: %s", frameID)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := LoadHTMLDocument(
|
doc, err := LoadHTMLDocument(
|
||||||
@ -427,16 +370,18 @@ func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) removeFrameInternal(frameID page.FrameID) error {
|
func (m *Manager) removeFrameInternal(frameID page.FrameID) error {
|
||||||
current, exists := m.frames[frameID]
|
current, exists := m.frames.Get(frameID)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return core.Error(core.ErrNotFound, "frame")
|
return core.Error(core.ErrNotFound, "frame")
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(m.frames, frameID)
|
m.frames.Remove(frameID)
|
||||||
|
|
||||||
if frameID == m.mainFrame {
|
mainFrameID := m.mainFrame.Get()
|
||||||
m.mainFrame = ""
|
|
||||||
|
if frameID == mainFrameID {
|
||||||
|
m.mainFrame.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
if current.node == nil {
|
if current.node == nil {
|
||||||
@ -447,7 +392,7 @@ func (m *Manager) removeFrameInternal(frameID page.FrameID) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) removeFrameRecursivelyInternal(frameID page.FrameID) error {
|
func (m *Manager) removeFrameRecursivelyInternal(frameID page.FrameID) error {
|
||||||
parent, exists := m.frames[frameID]
|
parent, exists := m.frames.Get(frameID)
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return core.Error(core.ErrNotFound, "frame")
|
return core.Error(core.ErrNotFound, "frame")
|
||||||
|
@ -24,6 +24,16 @@ type ExecutionContext struct {
|
|||||||
contextID runtime.ExecutionContextID
|
contextID runtime.ExecutionContextID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewExecutionContextFrom(ctx context.Context, client *cdp.Client, frame page.Frame) (*ExecutionContext, error) {
|
||||||
|
world, err := client.Page.CreateIsolatedWorld(ctx, page.NewCreateIsolatedWorldArgs(frame.ID))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewExecutionContext(client, frame, world.ExecutionContextID), nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewExecutionContext(client *cdp.Client, frame page.Frame, contextID runtime.ExecutionContextID) *ExecutionContext {
|
func NewExecutionContext(client *cdp.Client, frame page.Frame, contextID runtime.ExecutionContextID) *ExecutionContext {
|
||||||
ec := new(ExecutionContext)
|
ec := new(ExecutionContext)
|
||||||
ec.client = client
|
ec.client = client
|
||||||
|
@ -3,12 +3,9 @@ package events
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Loop struct {
|
type Loop struct {
|
||||||
mu sync.Mutex
|
|
||||||
cancel context.CancelFunc
|
|
||||||
sources *SourceCollection
|
sources *SourceCollection
|
||||||
listeners *ListenerCollection
|
listeners *ListenerCollection
|
||||||
}
|
}
|
||||||
@ -21,47 +18,8 @@ func NewLoop() *Loop {
|
|||||||
return loop
|
return loop
|
||||||
}
|
}
|
||||||
|
|
||||||
func (loop *Loop) Start() *Loop {
|
func (loop *Loop) Run(ctx context.Context) {
|
||||||
loop.mu.Lock()
|
go loop.run(ctx)
|
||||||
defer loop.mu.Unlock()
|
|
||||||
|
|
||||||
if loop.cancel != nil {
|
|
||||||
return loop
|
|
||||||
}
|
|
||||||
|
|
||||||
loopCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
loop.cancel = cancel
|
|
||||||
|
|
||||||
go loop.run(loopCtx)
|
|
||||||
|
|
||||||
return loop
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *Loop) Stop() *Loop {
|
|
||||||
loop.mu.Lock()
|
|
||||||
defer loop.mu.Unlock()
|
|
||||||
|
|
||||||
if loop.cancel == nil {
|
|
||||||
return loop
|
|
||||||
}
|
|
||||||
|
|
||||||
loop.cancel()
|
|
||||||
loop.cancel = nil
|
|
||||||
|
|
||||||
return loop
|
|
||||||
}
|
|
||||||
|
|
||||||
func (loop *Loop) Close() error {
|
|
||||||
loop.mu.Lock()
|
|
||||||
defer loop.mu.Unlock()
|
|
||||||
|
|
||||||
if loop.cancel != nil {
|
|
||||||
loop.cancel()
|
|
||||||
loop.cancel = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return loop.sources.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (loop *Loop) AddSource(source Source) {
|
func (loop *Loop) AddSource(source Source) {
|
||||||
@ -125,7 +83,6 @@ func (loop *Loop) run(ctx context.Context) {
|
|||||||
source = noop
|
source = noop
|
||||||
}
|
}
|
||||||
|
|
||||||
// commands have higher priority
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
@ -180,8 +180,10 @@ func TestLoop(t *testing.T) {
|
|||||||
|
|
||||||
loop.AddSource(src)
|
loop.AddSource(src)
|
||||||
|
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
onLoad.EmitDefault()
|
onLoad.EmitDefault()
|
||||||
|
|
||||||
@ -219,8 +221,10 @@ func TestLoop(t *testing.T) {
|
|||||||
counter.Increase()
|
counter.Increase()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
onLoad.EmitDefault()
|
onLoad.EmitDefault()
|
||||||
|
|
||||||
@ -252,8 +256,10 @@ func TestLoop(t *testing.T) {
|
|||||||
counter.Increase()
|
counter.Increase()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
onLoad := &TestLoadEventFiredClient{NewTestEventStream()}
|
onLoad := &TestLoadEventFiredClient{NewTestEventStream()}
|
||||||
|
|
||||||
@ -282,8 +288,10 @@ func TestLoop(t *testing.T) {
|
|||||||
loop := events.NewLoop()
|
loop := events.NewLoop()
|
||||||
counter := NewCounter()
|
counter := NewCounter()
|
||||||
|
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
loop.AddListener(TestEvent, events.Always(func(ctx context.Context, message interface{}) {
|
loop.AddListener(TestEvent, events.Always(func(ctx context.Context, message interface{}) {
|
||||||
counter.Increase()
|
counter.Increase()
|
||||||
@ -342,8 +350,10 @@ func TestLoop(t *testing.T) {
|
|||||||
return onLoad.Recv()
|
return onLoad.Recv()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
time.Sleep(time.Duration(100) * time.Millisecond)
|
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||||
|
|
||||||
@ -365,8 +375,10 @@ func BenchmarkLoop_AddListenerSync(b *testing.B) {
|
|||||||
|
|
||||||
func BenchmarkLoop_AddListenerAsync(b *testing.B) {
|
func BenchmarkLoop_AddListenerAsync(b *testing.B) {
|
||||||
loop := events.NewLoop()
|
loop := events.NewLoop()
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
loop.AddListener(TestEvent, events.Always(func(ctx context.Context, message interface{}) {}))
|
loop.AddListener(TestEvent, events.Always(func(ctx context.Context, message interface{}) {}))
|
||||||
@ -375,8 +387,10 @@ func BenchmarkLoop_AddListenerAsync(b *testing.B) {
|
|||||||
|
|
||||||
func BenchmarkLoop_AddListenerAsync2(b *testing.B) {
|
func BenchmarkLoop_AddListenerAsync2(b *testing.B) {
|
||||||
loop := events.NewLoop()
|
loop := events.NewLoop()
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
for pb.Next() {
|
for pb.Next() {
|
||||||
@ -417,8 +431,10 @@ func BenchmarkLoop_Start(b *testing.B) {
|
|||||||
return onLoad.Recv()
|
return onLoad.Recv()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
loop.Start()
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer loop.Stop()
|
|
||||||
|
loop.Run(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
onLoad.Emit(&page.LoadEventFiredReply{})
|
onLoad.Emit(&page.LoadEventFiredReply{})
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package events
|
package events
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@ -21,6 +22,10 @@ func (sc *SourceCollection) Close() error {
|
|||||||
sc.mu.Lock()
|
sc.mu.Lock()
|
||||||
defer sc.mu.Unlock()
|
defer sc.mu.Unlock()
|
||||||
|
|
||||||
|
if sc.values == nil {
|
||||||
|
return errors.New("sources are already closed")
|
||||||
|
}
|
||||||
|
|
||||||
errs := make([]error, 0, len(sc.values))
|
errs := make([]error, 0, len(sc.values))
|
||||||
|
|
||||||
for _, e := range sc.values {
|
for _, e := range sc.values {
|
||||||
@ -29,6 +34,8 @@ func (sc *SourceCollection) Close() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sc.values = nil
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return core.Errors(errs...)
|
return core.Errors(errs...)
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,17 @@ package network
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
|
||||||
"regexp"
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/mafredri/cdp"
|
"github.com/mafredri/cdp"
|
||||||
"github.com/mafredri/cdp/protocol/network"
|
"github.com/mafredri/cdp/protocol/network"
|
||||||
"github.com/mafredri/cdp/protocol/page"
|
"github.com/mafredri/cdp/protocol/page"
|
||||||
"github.com/mafredri/cdp/rpcc"
|
"github.com/mafredri/cdp/rpcc"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/MontFerret/ferret/pkg/drivers"
|
"github.com/MontFerret/ferret/pkg/drivers"
|
||||||
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
|
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
|
||||||
@ -84,7 +85,7 @@ func New(
|
|||||||
|
|
||||||
m.responseListenerID = m.eventLoop.AddListener(responseReceived, m.onResponse)
|
m.responseListenerID = m.eventLoop.AddListener(responseReceived, m.onResponse)
|
||||||
|
|
||||||
m.eventLoop.Start()
|
m.eventLoop.Run(ctx)
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@ -96,8 +97,6 @@ func (m *Manager) Close() error {
|
|||||||
if m.cancel != nil {
|
if m.cancel != nil {
|
||||||
m.cancel()
|
m.cancel()
|
||||||
m.cancel = nil
|
m.cancel = nil
|
||||||
|
|
||||||
return m.eventLoop.Stop().Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -326,10 +325,6 @@ func (m *Manager) WaitForNavigation(ctx context.Context, pattern *regexp.Regexp)
|
|||||||
func (m *Manager) WaitForFrameNavigation(ctx context.Context, frameID page.FrameID, urlPattern *regexp.Regexp) error {
|
func (m *Manager) WaitForFrameNavigation(ctx context.Context, frameID page.FrameID, urlPattern *regexp.Regexp) error {
|
||||||
onEvent := make(chan struct{})
|
onEvent := make(chan struct{})
|
||||||
|
|
||||||
defer func() {
|
|
||||||
close(onEvent)
|
|
||||||
}()
|
|
||||||
|
|
||||||
m.eventLoop.AddListener(eventFrameLoad, func(_ context.Context, message interface{}) bool {
|
m.eventLoop.AddListener(eventFrameLoad, func(_ context.Context, message interface{}) bool {
|
||||||
repl := message.(*page.FrameNavigatedReply)
|
repl := message.(*page.FrameNavigatedReply)
|
||||||
|
|
||||||
@ -348,7 +343,26 @@ func (m *Manager) WaitForFrameNavigation(ctx context.Context, frameID page.Frame
|
|||||||
|
|
||||||
if matched {
|
if matched {
|
||||||
if ctx.Err() == nil {
|
if ctx.Err() == nil {
|
||||||
|
ec, err := eval.NewExecutionContextFrom(ctx, m.client, repl.Frame)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
close(onEvent)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = events.NewEvalWaitTask(
|
||||||
|
ec,
|
||||||
|
templates.DOMReady(),
|
||||||
|
events.DefaultPolling,
|
||||||
|
).Run(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
close(onEvent)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
onEvent <- struct{}{}
|
onEvent <- struct{}{}
|
||||||
|
close(onEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,7 +288,17 @@ func (p *HTMLPage) Close() error {
|
|||||||
Msg("failed to close browser page")
|
Msg("failed to close browser page")
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.conn.Close()
|
err = p.conn.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
p.logger.Warn().
|
||||||
|
Timestamp().
|
||||||
|
Str("url", doc.GetURL().String()).
|
||||||
|
Err(err).
|
||||||
|
Msg("failed to close connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HTMLPage) IsClosed() values.Boolean {
|
func (p *HTMLPage) IsClosed() values.Boolean {
|
||||||
@ -511,16 +521,10 @@ func (p *HTMLPage) NavigateForward(ctx context.Context, skip values.Int) (values
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *HTMLPage) WaitForNavigation(ctx context.Context, targetURL values.String) error {
|
func (p *HTMLPage) WaitForNavigation(ctx context.Context, targetURL values.String) error {
|
||||||
var pattern *regexp.Regexp
|
pattern, err := p.urlToRegexp(targetURL)
|
||||||
|
|
||||||
if targetURL != "" {
|
if err != nil {
|
||||||
r, err := regexp.Compile(targetURL.String())
|
return err
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "invalid URL pattern")
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern = r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.network.WaitForNavigation(ctx, pattern); err != nil {
|
if err := p.network.WaitForNavigation(ctx, pattern); err != nil {
|
||||||
@ -530,11 +534,56 @@ func (p *HTMLPage) WaitForNavigation(ctx context.Context, targetURL values.Strin
|
|||||||
return p.reloadMainFrame(ctx)
|
return p.reloadMainFrame(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HTMLPage) reloadMainFrame(ctx context.Context) error {
|
func (p *HTMLPage) WaitForFrameNavigation(ctx context.Context, frame drivers.HTMLDocument, targetURL values.String) error {
|
||||||
if err := p.dom.WaitForDOMReady(ctx); err != nil {
|
current := p.dom.GetMainFrame()
|
||||||
|
doc, ok := frame.(*dom.HTMLDocument)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid frame type")
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern, err := p.urlToRegexp(targetURL)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frameID := doc.Frame().Frame.ID
|
||||||
|
isMain := current.Frame().Frame.ID == frameID
|
||||||
|
|
||||||
|
// if it's the current document
|
||||||
|
if isMain {
|
||||||
|
err = p.network.WaitForNavigation(ctx, pattern)
|
||||||
|
} else {
|
||||||
|
err = p.network.WaitForFrameNavigation(ctx, frameID, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//if isMain {
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
||||||
|
return p.reloadMainFrame(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLPage) urlToRegexp(targetURL values.String) (*regexp.Regexp, error) {
|
||||||
|
if targetURL == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := regexp.Compile(targetURL.String())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid URL pattern")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HTMLPage) reloadMainFrame(ctx context.Context) error {
|
||||||
prev := p.dom.GetMainFrame()
|
prev := p.dom.GetMainFrame()
|
||||||
|
|
||||||
next, err := dom.LoadRootHTMLDocument(
|
next, err := dom.LoadRootHTMLDocument(
|
||||||
|
13
pkg/drivers/cdp/templates/dom_ready.go
Normal file
13
pkg/drivers/cdp/templates/dom_ready.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package templates
|
||||||
|
|
||||||
|
const domReadyTemplate = `
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
`
|
||||||
|
|
||||||
|
func DOMReady() string {
|
||||||
|
return domReadyTemplate
|
||||||
|
}
|
@ -202,6 +202,10 @@ func (p *HTMLPage) WaitForNavigation(_ context.Context, _ values.String) error {
|
|||||||
return core.ErrNotSupported
|
return core.ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *HTMLPage) WaitForFrameNavigation(_ context.Context, _ drivers.HTMLDocument, _ values.String) error {
|
||||||
|
return core.ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
func (p *HTMLPage) Navigate(_ context.Context, _ values.String) error {
|
func (p *HTMLPage) Navigate(_ context.Context, _ values.String) error {
|
||||||
return core.ErrNotSupported
|
return core.ErrNotSupported
|
||||||
}
|
}
|
||||||
|
@ -206,6 +206,8 @@ type (
|
|||||||
|
|
||||||
WaitForNavigation(ctx context.Context, targetURL values.String) error
|
WaitForNavigation(ctx context.Context, targetURL values.String) error
|
||||||
|
|
||||||
|
WaitForFrameNavigation(ctx context.Context, frame HTMLDocument, targetURL values.String) error
|
||||||
|
|
||||||
Navigate(ctx context.Context, url values.String) error
|
Navigate(ctx context.Context, url values.String) error
|
||||||
|
|
||||||
NavigateBack(ctx context.Context, skip values.Int) (values.Boolean, error)
|
NavigateBack(ctx context.Context, skip values.Int) (values.Boolean, error)
|
||||||
|
@ -136,7 +136,19 @@ func (t *Array) ForEach(predicate ArrayPredicate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Array) Find(predicate ArrayPredicate) (core.Value, Boolean) {
|
func (t *Array) Find(predicate ArrayPredicate) (*Array, Boolean) {
|
||||||
|
result := NewArray(len(t.items))
|
||||||
|
|
||||||
|
for idx, val := range t.items {
|
||||||
|
if predicate(val, idx) {
|
||||||
|
result.Push(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, result.Length() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Array) FindOne(predicate ArrayPredicate) (core.Value, Boolean) {
|
||||||
for idx, val := range t.items {
|
for idx, val := range t.items {
|
||||||
if predicate(val, idx) {
|
if predicate(val, idx) {
|
||||||
return val, True
|
return val, True
|
||||||
|
@ -29,7 +29,7 @@ func Includes(ctx context.Context, args ...core.Value) (core.Value, error) {
|
|||||||
|
|
||||||
break
|
break
|
||||||
case *values.Array:
|
case *values.Array:
|
||||||
_, result = v.Find(func(value core.Value, _ int) bool {
|
_, result = v.FindOne(func(value core.Value, _ int) bool {
|
||||||
return needle.Compare(value) == 0
|
return needle.Compare(value) == 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
58
pkg/stdlib/html/find_frames.go
Normal file
58
pkg/stdlib/html/find_frames.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/MontFerret/ferret/pkg/drivers"
|
||||||
|
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||||
|
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FRAMES finds HTML frames by a given property selector.
|
||||||
|
// Returns an empty array if frames not found.
|
||||||
|
// @param page (HTMLPage) - HTML page.
|
||||||
|
// @param prop (String) - Property selector.
|
||||||
|
// @param value (Any) - Property value.
|
||||||
|
// @returns (Array) - Returns an array of found HTML frames.
|
||||||
|
func Frames(ctx context.Context, args ...core.Value) (core.Value, error) {
|
||||||
|
err := core.ValidateArgs(args, 3, 3)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := drivers.ToPage(args[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
frames, err := page.GetFrames(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return values.None, err
|
||||||
|
}
|
||||||
|
|
||||||
|
propName := values.ToString(args[1])
|
||||||
|
propValue := args[2]
|
||||||
|
|
||||||
|
result, _ := frames.Find(func(value core.Value, idx int) bool {
|
||||||
|
doc, e := drivers.ToDocument(value)
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPropValue, e := doc.GetIn(ctx, []core.Value{propName})
|
||||||
|
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentPropValue.Compare(propValue) == 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
@ -28,6 +28,7 @@ func RegisterLib(ns core.Namespace) error {
|
|||||||
"ELEMENT_EXISTS": ElementExists,
|
"ELEMENT_EXISTS": ElementExists,
|
||||||
"ELEMENTS": Elements,
|
"ELEMENTS": Elements,
|
||||||
"ELEMENTS_COUNT": ElementsCount,
|
"ELEMENTS_COUNT": ElementsCount,
|
||||||
|
"FRAMES": Frames,
|
||||||
"FOCUS": Focus,
|
"FOCUS": Focus,
|
||||||
"HOVER": Hover,
|
"HOVER": Hover,
|
||||||
"INNER_HTML": GetInnerHTML,
|
"INNER_HTML": GetInnerHTML,
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
type WaitNavigationParams struct {
|
type WaitNavigationParams struct {
|
||||||
TargetURL values.String
|
TargetURL values.String
|
||||||
Timeout values.Int
|
Timeout values.Int
|
||||||
|
Frame drivers.HTMLDocument
|
||||||
}
|
}
|
||||||
|
|
||||||
// WAIT_NAVIGATION waits for a given page to navigate to a new url.
|
// WAIT_NAVIGATION waits for a given page to navigate to a new url.
|
||||||
@ -49,7 +50,11 @@ func WaitNavigation(ctx context.Context, args ...core.Value) (core.Value, error)
|
|||||||
ctx, fn := waitTimeout(ctx, params.Timeout)
|
ctx, fn := waitTimeout(ctx, params.Timeout)
|
||||||
defer fn()
|
defer fn()
|
||||||
|
|
||||||
return values.None, doc.WaitForNavigation(ctx, params.TargetURL)
|
if params.Frame == nil {
|
||||||
|
return values.None, doc.WaitForNavigation(ctx, params.TargetURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values.None, doc.WaitForFrameNavigation(ctx, params.Frame, params.TargetURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseWaitNavigationParams(arg core.Value) (WaitNavigationParams, error) {
|
func parseWaitNavigationParams(arg core.Value) (WaitNavigationParams, error) {
|
||||||
@ -62,7 +67,6 @@ func parseWaitNavigationParams(arg core.Value) (WaitNavigationParams, error) {
|
|||||||
|
|
||||||
if arg.Type() == types.Int {
|
if arg.Type() == types.Int {
|
||||||
params.Timeout = arg.(values.Int)
|
params.Timeout = arg.(values.Int)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
obj := arg.(*values.Object)
|
obj := arg.(*values.Object)
|
||||||
|
|
||||||
@ -85,6 +89,16 @@ func parseWaitNavigationParams(arg core.Value) (WaitNavigationParams, error) {
|
|||||||
|
|
||||||
params.TargetURL = v.(values.String)
|
params.TargetURL = v.(values.String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v, exists := obj.Get("frame"); exists {
|
||||||
|
doc, err := drivers.ToDocument(v)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return params, errors.Wrap(err, "navigation parameters: frame")
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Frame = doc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return params, nil
|
return params, nil
|
||||||
|
Reference in New Issue
Block a user