1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-04-17 12:06:17 +02:00

Feature/#577 spa routing (#584)

* Added support of getting URL dynamically
This commit is contained in:
Tim Voronov 2021-02-16 09:49:26 -05:00 committed by GitHub
parent f4876c05a3
commit ff8c15eb67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 66 additions and 198 deletions

View File

@ -11,16 +11,16 @@ export default function Layout({ children }) {
e("div", { className: "collapse navbar-collapse" }, [ e("div", { className: "collapse navbar-collapse" }, [
e("ul", { className: "navbar-nav mr-auto" }, [ e("ul", { className: "navbar-nav mr-auto" }, [
e("li", { className: "nav-item"}, [ e("li", { className: "nav-item"}, [
e(NavLink, { className: "nav-link", to: "/forms" }, "Forms") e(NavLink, { className: "nav-link nav-link-forms", to: "/forms" }, "Forms")
]), ]),
e("li", { className: "nav-item"}, [ e("li", { className: "nav-item"}, [
e(NavLink, { className: "nav-link", to: "/navigation" }, "Navigation") e(NavLink, { className: "nav-link nav-link-navigation", to: "/navigation" }, "Navigation")
]), ]),
e("li", { className: "nav-item"}, [ e("li", { className: "nav-item"}, [
e(NavLink, { className: "nav-link", to: "/events" }, "Events") e(NavLink, { className: "nav-link nav-link-events", to: "/events" }, "Events")
]), ]),
e("li", { className: "nav-item"}, [ e("li", { className: "nav-item"}, [
e(NavLink, { className: "nav-link", to: "/iframe" }, "iFrame") e(NavLink, { className: "nav-link nav-link-iframe", to: "/iframe" }, "iFrame")
]) ])
]) ])
]) ])

View File

@ -1,5 +1,5 @@
LET url = @lab.cdn.dynamic LET url = @lab.cdn.dynamic
LET doc = DOCUMENT(url, true) LET doc = DOCUMENT(url, { driver: "cdp" })
LET expected = `<head> LET expected = `<head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -11,7 +11,7 @@ LET expected = `<head>
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css">
</head> </head>
<body class="text-center"> <body class="text-center">
<div id="root"><div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="#/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="#/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="#/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="#/events">Events</a></li><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> <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 nav-link-forms" href="#/forms">Forms</a></li><li class="nav-item"><a class="nav-link nav-link-navigation" href="#/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link nav-link-events" href="#/events">Events</a></li><li class="nav-item"><a class="nav-link nav-link-iframe" 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@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/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/history@4.9.0/umd/history.min.js"></script>
@ -19,8 +19,6 @@ LET expected = `<head>
<script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script> <script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script>
<script src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js" crossorigin=""> </script> <script src="https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js" crossorigin=""> </script>
<script src="index.js" type="module"></script> <script src="index.js" type="module"></script>
</body>` </body>`
LET actual = INNER_HTML(doc) LET actual = INNER_HTML(doc)

View File

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

View File

@ -1,7 +1,7 @@
LET url = @lab.cdn.dynamic LET url = @lab.cdn.dynamic
LET doc = DOCUMENT(url, { driver: "cdp" }) LET doc = DOCUMENT(url, { driver: "cdp" })
LET expected = url + '/' LET expected = url + '/#/'
LET actual = doc.url LET actual = doc.url
T::EQ(actual, expected) T::EQ(actual, expected)

View File

@ -3,4 +3,4 @@ LET p = DOCUMENT("", { driver: "cdp" })
NAVIGATE(p, url) NAVIGATE(p, url)
RETURN T::EQ(p.url, url + '/') RETURN T::EQ(p.url, url + '/#/')

View File

@ -0,0 +1,16 @@
LET page = DOCUMENT(@lab.cdn.dynamic, {
driver: 'cdp'
})
LET initialDoc = page.frames[0].url
LET initial = page.url
CLICK(page, ".nav-link-forms")
LET currentDoc = page.frames[0].url
LET current = page.url
T::NOT::EQ(initial, current)
T::EQ(initialDoc, currentDoc)
RETURN NONE

View File

@ -454,6 +454,10 @@ func (doc *HTMLDocument) ScrollByXY(ctx context.Context, x, y values.Float, opti
return doc.input.ScrollByXY(ctx, float64(x), float64(y), options) return doc.input.ScrollByXY(ctx, float64(x), float64(y), options)
} }
func (doc *HTMLDocument) Eval(ctx context.Context, expression string) (core.Value, error) {
return doc.exec.EvalWithReturnValue(ctx, expression)
}
func (doc *HTMLDocument) logError(err error) *zerolog.Event { func (doc *HTMLDocument) logError(err error) *zerolog.Event {
return doc.logger. return doc.logger.
Error(). Error().

View File

@ -2,157 +2,44 @@ package dom
import ( import (
"context" "context"
"io"
"sync" "sync"
"github.com/mafredri/cdp" "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"
"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/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values" "github.com/MontFerret/ferret/pkg/runtime/values"
) )
var (
eventDocumentUpdated = events.New("doc_updated")
eventChildNodeInserted = events.New("child_inserted")
eventChildNodeRemoved = events.New("child_removed")
)
type ( type (
DocumentUpdatedListener func(ctx context.Context)
AttrModifiedListener func(ctx context.Context, nodeID dom.NodeID, name, value string)
AttrRemovedListener func(ctx context.Context, nodeID dom.NodeID, name string)
ChildNodeCountUpdatedListener func(ctx context.Context, nodeID dom.NodeID, count int)
ChildNodeInsertedListener func(ctx context.Context, nodeID, previousNodeID dom.NodeID, node dom.Node)
ChildNodeRemovedListener func(ctx context.Context, nodeID, previousNodeID dom.NodeID)
Manager struct { Manager struct {
mu sync.RWMutex mu sync.RWMutex
logger *zerolog.Logger logger *zerolog.Logger
client *cdp.Client client *cdp.Client
events *events.Loop
mouse *input.Mouse mouse *input.Mouse
keyboard *input.Keyboard keyboard *input.Keyboard
mainFrame *AtomicFrameID mainFrame *AtomicFrameID
frames *AtomicFrameCollection frames *AtomicFrameCollection
cancel context.CancelFunc
} }
) )
// a dirty workaround to let pass the vet test
func createContext() (context.Context, context.CancelFunc) {
return context.WithCancel(context.Background())
}
func New( func New(
logger *zerolog.Logger, logger *zerolog.Logger,
client *cdp.Client, client *cdp.Client,
mouse *input.Mouse, mouse *input.Mouse,
keyboard *input.Keyboard, keyboard *input.Keyboard,
) (manager *Manager, err error) { ) (manager *Manager, err error) {
ctx, cancel := createContext()
closers := make([]io.Closer, 0, 10)
defer func() {
if err != nil {
common.CloseAll(logger, closers, "failed to close a DOM event stream")
}
}()
onContentReady, err := client.Page.DOMContentEventFired(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onContentReady)
onDocUpdated, err := client.DOM.DocumentUpdated(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onDocUpdated)
onAttrModified, err := client.DOM.AttributeModified(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onAttrModified)
onAttrRemoved, err := client.DOM.AttributeRemoved(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onAttrRemoved)
onChildCountUpdated, err := client.DOM.ChildNodeCountUpdated(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onChildCountUpdated)
onChildNodeInserted, err := client.DOM.ChildNodeInserted(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onChildNodeInserted)
onChildNodeRemoved, err := client.DOM.ChildNodeRemoved(ctx)
if err != nil {
return nil, err
}
closers = append(closers, onChildNodeRemoved)
eventLoop := events.NewLoop()
eventLoop.AddSource(events.NewSource(eventDocumentUpdated, onDocUpdated, func(stream rpcc.Stream) (i interface{}, e error) {
return stream.(dom.DocumentUpdatedClient).Recv()
}))
eventLoop.AddSource(events.NewSource(eventChildNodeInserted, onChildNodeInserted, func(stream rpcc.Stream) (i interface{}, e error) {
return stream.(dom.ChildNodeInsertedClient).Recv()
}))
eventLoop.AddSource(events.NewSource(eventChildNodeRemoved, onChildNodeRemoved, func(stream rpcc.Stream) (i interface{}, e error) {
return stream.(dom.ChildNodeRemovedClient).Recv()
}))
manager = new(Manager) manager = new(Manager)
manager.logger = logger manager.logger = logger
manager.client = client manager.client = client
manager.events = eventLoop
manager.mouse = mouse manager.mouse = mouse
manager.keyboard = keyboard manager.keyboard = keyboard
manager.mainFrame = NewAtomicFrameID() manager.mainFrame = NewAtomicFrameID()
manager.frames = NewAtomicFrameCollection() manager.frames = NewAtomicFrameCollection()
manager.cancel = cancel
eventLoop.Run(ctx)
return manager, nil return manager, nil
} }
@ -160,11 +47,6 @@ func New(
func (m *Manager) Close() error { func (m *Manager) Close() error {
errs := make([]error, 0, m.frames.Length()+1) errs := make([]error, 0, m.frames.Length()+1)
if m.cancel != nil {
m.cancel()
m.cancel = nil
}
m.frames.ForEach(func(f Frame, key page.FrameID) bool { m.frames.ForEach(func(f Frame, key page.FrameID) bool {
// if initialized // if initialized
if f.node != nil { if f.node != nil {
@ -295,64 +177,6 @@ func (m *Manager) GetFrameNodes(ctx context.Context) (*values.Array, error) {
return arr, nil return arr, nil
} }
func (m *Manager) AddDocumentUpdatedListener(listener DocumentUpdatedListener) events.ListenerID {
m.mu.RLock()
defer m.mu.RUnlock()
return m.events.AddListener(eventDocumentUpdated, func(ctx context.Context, _ interface{}) bool {
listener(ctx)
return true
})
}
func (m *Manager) RemoveReloadListener(listenerID events.ListenerID) {
m.mu.RLock()
defer m.mu.RUnlock()
m.events.RemoveListener(eventDocumentUpdated, listenerID)
}
func (m *Manager) AddChildNodeInsertedListener(listener ChildNodeInsertedListener) events.ListenerID {
m.mu.RLock()
defer m.mu.RUnlock()
return m.events.AddListener(eventChildNodeInserted, func(ctx context.Context, message interface{}) bool {
reply := message.(*dom.ChildNodeInsertedReply)
listener(ctx, reply.ParentNodeID, reply.PreviousNodeID, reply.Node)
return true
})
}
func (m *Manager) RemoveChildNodeInsertedListener(listenerID events.ListenerID) {
m.mu.RLock()
defer m.mu.RUnlock()
m.events.RemoveListener(eventChildNodeInserted, listenerID)
}
func (m *Manager) AddChildNodeRemovedListener(listener ChildNodeRemovedListener) events.ListenerID {
m.mu.RLock()
defer m.mu.RUnlock()
return m.events.AddListener(eventChildNodeRemoved, func(ctx context.Context, message interface{}) bool {
reply := message.(*dom.ChildNodeRemovedReply)
listener(ctx, reply.ParentNodeID, reply.NodeID)
return true
})
}
func (m *Manager) RemoveChildNodeRemovedListener(listenerID events.ListenerID) {
m.mu.RLock()
defer m.mu.RUnlock()
m.events.RemoveListener(eventChildNodeRemoved, listenerID)
}
func (m *Manager) addFrameInternal(frame page.FrameTree) { func (m *Manager) addFrameInternal(frame page.FrameTree) {
m.frames.Set(frame.Frame.ID, Frame{ m.frames.Set(frame.Frame.ID, Frame{
tree: frame, tree: frame,

View File

@ -2,6 +2,7 @@ package cdp
import ( import (
"context" "context"
"github.com/MontFerret/ferret/pkg/drivers/cdp/templates"
"hash/fnv" "hash/fnv"
"io" "io"
"regexp" "regexp"
@ -262,16 +263,15 @@ func (p *HTMLPage) Close() error {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
url := p.GetURL().String()
p.closed = values.True p.closed = values.True
doc := p.getCurrentDocument()
err := p.dom.Close() err := p.dom.Close()
if err != nil { if err != nil {
p.logger.Warn(). p.logger.Warn().
Timestamp(). Timestamp().
Str("url", doc.GetURL().String()). Str("url", url).
Err(err). Err(err).
Msg("failed to close dom manager") Msg("failed to close dom manager")
} }
@ -281,7 +281,7 @@ func (p *HTMLPage) Close() error {
if err != nil { if err != nil {
p.logger.Warn(). p.logger.Warn().
Timestamp(). Timestamp().
Str("url", doc.GetURL().String()). Str("url", url).
Err(err). Err(err).
Msg("failed to close network manager") Msg("failed to close network manager")
} }
@ -291,7 +291,7 @@ func (p *HTMLPage) Close() error {
if err != nil { if err != nil {
p.logger.Warn(). p.logger.Warn().
Timestamp(). Timestamp().
Str("url", doc.GetURL().String()). Str("url", url).
Err(err). Err(err).
Msg("failed to close browser page") Msg("failed to close browser page")
} }
@ -301,7 +301,7 @@ func (p *HTMLPage) Close() error {
if err != nil { if err != nil {
p.logger.Warn(). p.logger.Warn().
Timestamp(). Timestamp().
Str("url", doc.GetURL().String()). Str("url", url).
Err(err). Err(err).
Msg("failed to close connection") Msg("failed to close connection")
} }
@ -317,6 +317,17 @@ func (p *HTMLPage) IsClosed() values.Boolean {
} }
func (p *HTMLPage) GetURL() values.String { func (p *HTMLPage) GetURL() values.String {
res, err := p.getCurrentDocument().Eval(context.Background(), templates.GetURL())
if err == nil {
return values.ToString(res)
}
p.logger.Warn().
Timestamp().
Err(err).
Msg("failed to retrieve URL")
return p.getCurrentDocument().GetURL() return p.getCurrentDocument().GetURL()
} }

View File

@ -0,0 +1,7 @@
package templates
const getURL = `return window.location.toString()`
func GetURL() string {
return getURL
}

View File

@ -34,10 +34,18 @@ func (av *AtomicValue) Read() core.Value {
} }
// Write sets a new underlying value. // Write sets a new underlying value.
func (av *AtomicValue) Write(next core.Value) {
av.mu.Lock()
defer av.mu.Unlock()
av.value = next
}
// WriteWith sets a new underlying value with a custom writer.
// If writer fails, the operations gets terminated and an underlying value remains. // If writer fails, the operations gets terminated and an underlying value remains.
// @param (AtomicValueWriter) - Writer function that receives a current value and returns new one. // @param (AtomicValueWriter) - Writer function that receives a current value and returns new one.
// @returns (Error) - Error if write operation failed // @returns (Error) - Error if write operation failed
func (av *AtomicValue) Write(writer AtomicValueWriter) error { func (av *AtomicValue) WriteWith(writer AtomicValueWriter) error {
av.mu.Lock() av.mu.Lock()
defer av.mu.Unlock() defer av.mu.Unlock()

View File

@ -61,7 +61,7 @@ func GetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value) (c
return GetInDocument(ctx, frame, path[2:]) return GetInDocument(ctx, frame, path[2:])
case "url", "URL": case "url", "URL":
return page.GetMainFrame().GetURL(), nil return page.GetURL(), nil
case "cookies": case "cookies":
cookies, err := page.GetCookies(ctx) cookies, err := page.GetCookies(ctx)
@ -74,10 +74,10 @@ func GetInPage(ctx context.Context, page drivers.HTMLPage, path []core.Value) (c
} }
return cookies.GetIn(ctx, path[1:]) return cookies.GetIn(ctx, path[1:])
case "isClosed":
return page.IsClosed(), nil
case "title": case "title":
return page.GetMainFrame().GetTitle(), nil return page.GetMainFrame().GetTitle(), nil
case "isClosed":
return page.IsClosed(), nil
default: default:
return GetInDocument(ctx, page.GetMainFrame(), path) return GetInDocument(ctx, page.GetMainFrame(), path)
} }