1
0
mirror of https://github.com/MontFerret/ferret.git synced 2024-12-04 10:35:08 +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("ul", { className: "navbar-nav mr-auto" }, [
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(NavLink, { className: "nav-link", to: "/navigation" }, "Navigation")
e(NavLink, { className: "nav-link nav-link-navigation", to: "/navigation" }, "Navigation")
]),
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(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 doc = DOCUMENT(url, true)
LET doc = DOCUMENT(url, { driver: "cdp" })
LET expected = `<head>
<meta charset="utf-8">
@ -11,7 +11,7 @@ LET expected = `<head>
<link rel="stylesheet" href="index.css">
</head>
<body class="text-center">
<div id="root"><div id="layout"><nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4" id="navbar"><a class="navbar-brand active" aria-current="page" href="#/">Ferret</a><button class="navbar-toggler" type="button"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="#/forms">Forms</a></li><li class="nav-item"><a class="nav-link" href="#/navigation">Navigation</a></li><li class="nav-item"><a class="nav-link" href="#/events">Events</a></li><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-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>
@ -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-bootstrap@next/dist/react-bootstrap.min.js" crossorigin=""> </script>
<script src="index.js" type="module"></script>
</body>`
LET actual = INNER_HTML(doc)

View File

@ -1,8 +1,8 @@
LET url = @lab.cdn.dynamic
LET doc = DOCUMENT(url, true)
LET doc = DOCUMENT(url, { driver: "cdp" })
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 r1 = '(\s|\")'

View File

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

View File

@ -3,4 +3,4 @@ LET p = DOCUMENT("", { driver: "cdp" })
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)
}
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 {
return doc.logger.
Error().

View File

@ -2,157 +2,44 @@ package dom
import (
"context"
"io"
"sync"
"github.com/mafredri/cdp"
"github.com/mafredri/cdp/protocol/dom"
"github.com/mafredri/cdp/protocol/page"
"github.com/mafredri/cdp/rpcc"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/MontFerret/ferret/pkg/drivers/cdp/events"
"github.com/MontFerret/ferret/pkg/drivers/cdp/input"
"github.com/MontFerret/ferret/pkg/drivers/common"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
)
var (
eventDocumentUpdated = events.New("doc_updated")
eventChildNodeInserted = events.New("child_inserted")
eventChildNodeRemoved = events.New("child_removed")
)
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 {
mu sync.RWMutex
logger *zerolog.Logger
client *cdp.Client
events *events.Loop
mouse *input.Mouse
keyboard *input.Keyboard
mainFrame *AtomicFrameID
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(
logger *zerolog.Logger,
client *cdp.Client,
mouse *input.Mouse,
keyboard *input.Keyboard,
) (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.logger = logger
manager.client = client
manager.events = eventLoop
manager.mouse = mouse
manager.keyboard = keyboard
manager.mainFrame = NewAtomicFrameID()
manager.frames = NewAtomicFrameCollection()
manager.cancel = cancel
eventLoop.Run(ctx)
return manager, nil
}
@ -160,11 +47,6 @@ func New(
func (m *Manager) Close() error {
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 {
// if initialized
if f.node != nil {
@ -295,64 +177,6 @@ func (m *Manager) GetFrameNodes(ctx context.Context) (*values.Array, error) {
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) {
m.frames.Set(frame.Frame.ID, Frame{
tree: frame,

View File

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

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.
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.
// @param (AtomicValueWriter) - Writer function that receives a current value and returns new one.
// @returns (Error) - Error if write operation failed
func (av *AtomicValue) Write(writer AtomicValueWriter) error {
func (av *AtomicValue) WriteWith(writer AtomicValueWriter) error {
av.mu.Lock()
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:])
case "url", "URL":
return page.GetMainFrame().GetURL(), nil
return page.GetURL(), nil
case "cookies":
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:])
case "isClosed":
return page.IsClosed(), nil
case "title":
return page.GetMainFrame().GetTitle(), nil
case "isClosed":
return page.IsClosed(), nil
default:
return GetInDocument(ctx, page.GetMainFrame(), path)
}