1
0
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:
Tim Voronov
2020-07-13 14:13:03 -04:00
committed by GitHub
parent 079ec3a3ce
commit 162dd07346
30 changed files with 530 additions and 274 deletions

View File

@ -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

View File

@ -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}/...

View File

@ -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 {

View File

@ -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: {

View 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 ""

View 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 ""

View 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")

View 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")

View File

@ -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

View 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/")

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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 {

View 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
}

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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{})

View File

@ -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...)
} }

View File

@ -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)
} }
} }

View File

@ -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(

View File

@ -0,0 +1,13 @@
package templates
const domReadyTemplate = `
if (document.readyState === 'complete') {
return true;
}
return null;
`
func DOMReady() string {
return domReadyTemplate
}

View File

@ -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
} }

View File

@ -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)

View File

@ -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

View File

@ -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
}) })

View 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
}

View File

@ -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,

View File

@ -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