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:
parent
f4876c05a3
commit
ff8c15eb67
@ -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")
|
||||
])
|
||||
])
|
||||
])
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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|\")'
|
||||
|
@ -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)
|
@ -3,4 +3,4 @@ LET p = DOCUMENT("", { driver: "cdp" })
|
||||
|
||||
NAVIGATE(p, url)
|
||||
|
||||
RETURN T::EQ(p.url, url + '/')
|
||||
RETURN T::EQ(p.url, url + '/#/')
|
16
e2e/tests/dynamic/page/url/get.fql
Normal file
16
e2e/tests/dynamic/page/url/get.fql
Normal 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
|
@ -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().
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
7
pkg/drivers/cdp/templates/url.go
Normal file
7
pkg/drivers/cdp/templates/url.go
Normal file
@ -0,0 +1,7 @@
|
||||
package templates
|
||||
|
||||
const getURL = `return window.location.toString()`
|
||||
|
||||
func GetURL() string {
|
||||
return getURL
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user