1
0
mirror of https://github.com/MontFerret/ferret.git synced 2025-05-31 23:09:43 +02:00

Feature/#229 wait no element (#249)

* Added possibility to wait for an element or a class absence
This commit is contained in:
Tim Voronov 2019-03-06 21:52:41 -05:00 committed by GitHub
parent 5188f9e71b
commit d0caef8be7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 385 additions and 102 deletions

View File

@ -54,6 +54,7 @@ jobs:
script:
- make cover
- stage: e2e
go: stable
before_script:
- google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 --disable-setuid-sandbox --no-sandbox about:blank &
script:

View File

@ -1,12 +1,14 @@
package main
import (
"context"
"flag"
"fmt"
"github.com/MontFerret/ferret/e2e/runner"
"github.com/MontFerret/ferret/e2e/server"
"github.com/rs/zerolog"
"os"
"os/signal"
"path/filepath"
"regexp"
)
@ -101,7 +103,18 @@ func main() {
Filter: filterR,
})
err := r.Run()
ctx, cancel := context.WithCancel(context.Background())
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for {
<-c
cancel()
}
}()
err := r.Run(ctx)
if err != nil {
os.Exit(1)

View File

@ -0,0 +1,42 @@
import random from "../../../utils/random.js";
const e = React.createElement;
function render(id) {
return e("span", { id: `${id}-content` }, ["Hello world"]);
}
export default class AppearableComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
element: props.appear === true ? null : render(props.id)
};
}
handleClick() {
setTimeout(() => {
this.setState({
element: this.props.appear === true ? render(this.props.id) : null
})
}, random())
}
render() {
const btnId = `${this.props.id}-btn`;
return e("div", {className: "card"}, [
e("div", { className: "card-header"}, [
e("button", {
id: btnId,
className: "btn btn-primary",
onClick: this.handleClick.bind(this)
}, [
this.props.title || "Toggle class"
])
]),
e("div", { className: "card-body"}, this.state.element)
]);
}
}

View File

@ -1,3 +1,5 @@
import random from "../../../utils/random.js";
const e = React.createElement;
export default class ClickableComponent extends React.PureComponent {
@ -5,7 +7,7 @@ export default class ClickableComponent extends React.PureComponent {
super(props);
this.state = {
clicked: false
show: props.show === true
};
}
@ -13,12 +15,12 @@ export default class ClickableComponent extends React.PureComponent {
let timeout = 500;
if (this.props.randomTimeout) {
timeout = Math.ceil(Math.random() * 1000 * 10);
timeout = random();
}
setTimeout(() => {
this.setState({
clicked: !this.state.clicked
show: !this.state.show
})
}, timeout)
}
@ -28,7 +30,7 @@ export default class ClickableComponent extends React.PureComponent {
const contentId = `${this.props.id}-content`;
const classNames = ["alert"];
if (this.state.clicked) {
if (this.state.show === true) {
classNames.push("alert-success");
}
@ -39,7 +41,7 @@ export default class ClickableComponent extends React.PureComponent {
className: "btn btn-primary",
onClick: this.handleClick.bind(this)
}, [
"Toggle class"
this.props.title || "Toggle class"
])
]),
e("div", { className: "card-body"}, [

View File

@ -1,19 +1,60 @@
import Hoverable from "./hoverable.js";
import Clickable from "./clickable.js";
import Appearable from "./appearable.js";
const e = React.createElement;
export default class EventsPage extends React.Component {
render() {
return e("div", { className: "row", id: "page-events" }, [
e("div", { className: "col-lg-4"}, [
e(Hoverable),
return e("div", { id: "page-events" }, [
e("div", { className: "row" }, [
e("div", { className: "col-lg-4"}, [
e(Hoverable),
]),
e("div", { className: "col-lg-4"}, [
e(Clickable, {
id: "wait-class",
title: "Add class"
})
]),
e("div", { className: "col-lg-4"}, [
e(Clickable, {
id: "wait-class-random",
title: "Add class 2",
randomTimeout: true
})
])
]),
e("div", { className: "col-lg-4"}, [
e(Clickable, { id: "wait-class" })
]),
e("div", { className: "col-lg-4"}, [
e(Clickable, { id: "wait-class-random", randomTimeout: true })
e("div", { className: "row" }, [
e("div", { className: "col-lg-4"}, [
e(Clickable, {
id: "wait-no-class",
title: "Remove class",
show: true
})
]),
e("div", { className: "col-lg-4"}, [
e(Clickable, {
id: "wait-no-class-random",
title: "Remove class 2",
show: true,
randomTimeout: true
})
]),
e("div", { className: "col-lg-4"}, [
e(Appearable, {
id: "wait-element",
appear: true,
title: "Appearable"
})
]),
e("div", { className: "col-lg-4"}, [
e(Appearable, {
id: "wait-no-element",
appear: false,
title: "Disappearable"
})
])
])
])
}

View File

@ -0,0 +1,13 @@
export default function random(min = 1000, max = 5000) {
const val = Math.random() * 1000 * 10;
if (val < min) {
return min;
}
if (val > max) {
return max;
}
return val;
}

View File

@ -52,9 +52,7 @@ func New(logger zerolog.Logger, settings Settings) *Runner {
}
}
func (r *Runner) Run() error {
ctx := context.Background()
func (r *Runner) Run(ctx context.Context) error {
ctx = drivers.WithContext(
ctx,
cdp.NewDriver(cdp.WithAddress(r.settings.CDPAddress)),

View File

@ -3,7 +3,7 @@ LET doc = DOCUMENT(url, true)
WAIT_ELEMENT(doc, "#page-events")
CLICK_ALL(doc, ".clickable button")
WAIT_CLASS_ALL(doc, ".clickable .card-body div", "alert-success", 10000)
CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
WAIT_CLASS_ALL(doc, "#wait-class-content, #wait-class-random-content", "alert-success", 10000)
RETURN ""

View File

@ -0,0 +1,13 @@
LET url = @dynamic + "?redirect=/events"
LET doc = DOCUMENT(url, true)
LET pageSelector = "#page-events"
LET elemSelector = "#wait-element-content"
LET btnSelector = "#wait-element-btn"
WAIT_ELEMENT(doc, pageSelector)
CLICK(doc, btnSelector)
WAIT_ELEMENT(doc, elemSelector, 10000)
RETURN ELEMENT_EXISTS(doc, elemSelector) ? "" : "element not found"

View File

@ -0,0 +1,9 @@
LET url = @dynamic + "?redirect=/events"
LET doc = DOCUMENT(url, true)
WAIT_ELEMENT(doc, "#page-events")
CLICK_ALL(doc, "#wait-no-class-btn, #wait-no-class-random-btn")
WAIT_NO_CLASS_ALL(doc, "#wait-no-class-content, #wait-no-class-random-content", "alert-success", 10000)
RETURN ""

View File

@ -0,0 +1,14 @@
LET url = @dynamic + "?redirect=/events"
LET doc = DOCUMENT(url, true)
WAIT_ELEMENT(doc, "#page-events")
// with fixed timeout
CLICK(doc, "#wait-no-class-btn")
WAIT_NO_CLASS(doc, "#wait-no-class-content", "alert-success")
// with random timeout
CLICK(doc, "#wait-no-class-random-btn")
WAIT_NO_CLASS(doc, "#wait-no-class-random-content", "alert-success", 10000)
RETURN ""

View File

@ -0,0 +1,13 @@
LET url = @dynamic + "?redirect=/events"
LET doc = DOCUMENT(url, true)
LET pageSelector = "#page-events"
LET elemSelector = "#wait-no-element-content"
LET btnSelector = "#wait-no-element-btn"
WAIT_ELEMENT(doc, pageSelector)
CLICK(doc, btnSelector)
WAIT_NO_ELEMENT(doc, elemSelector, 10000)
RETURN ELEMENT_EXISTS(doc, elemSelector) ? "element should not be found" : ""

View File

@ -0,0 +1,20 @@
LET url = @dynamic + "?redirect=/events"
LET doc = DOCUMENT(url, true)
WAIT_ELEMENT(doc, "#page-events")
// with fixed timeout
LET b1 = ELEMENT(doc, "#wait-no-class-btn")
LET c1 = ELEMENT(doc, "#wait-no-class-content")
CLICK(b1)
WAIT_NO_CLASS(c1, "alert-success")
// with random timeout
LET b2 = ELEMENT(doc, "#wait-no-class-random-btn")
LET c2 = ELEMENT(doc, "#wait-no-class-random-content")
CLICK(b2)
WAIT_NO_CLASS(c2, "alert-success", 10000)
RETURN ""

View File

@ -526,44 +526,23 @@ func (doc *HTMLDocument) MoveMouseByXY(ctx context.Context, x, y values.Float) e
)
}
func (doc *HTMLDocument) WaitForSelector(ctx context.Context, selector values.String) error {
func (doc *HTMLDocument) WaitForElement(ctx context.Context, selector values.String, when drivers.WaitEvent) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el != null) {
return true;
}
// null means we need to repeat
return null;
`, eval.ParamString(selector.String())),
events.DefaultPolling,
)
_, err := task.Run(ctx)
return err
}
func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector, class values.String) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el == null) {
return false;
}
var className = %s;
var found = el.className.split(' ').find(i => i === className);
if (found != null) {
return true;
}
// null means we need to repeat
return null;
`,
fmt.Sprintf(
`
var el = document.querySelector(%s);
if (el %s null) {
return true;
}
// null means we need to repeat
return null;
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
waitEventToEqOperator(when),
),
events.DefaultPolling,
)
@ -573,22 +552,58 @@ func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector, c
return err
}
func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector, class values.String) error {
func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var el = document.querySelector(%s);
if (el == null) {
return false;
}
var className = %s;
var found = el.className.split(' ').find(i => i === className);
if (found %s null) {
return true;
}
// null means we need to repeat
return null;
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
waitEventToEqOperator(when),
),
events.DefaultPolling,
)
_, err := task.Run(ctx)
return err
}
func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error {
task := events.NewEvalWaitTask(
doc.client,
fmt.Sprintf(`
var elements = document.querySelectorAll(%s);
if (elements == null || elements.length === 0) {
return false;
}
var className = %s;
var foundCount = 0;
elements.forEach((el) => {
var found = el.className.split(' ').find(i => i === className);
if (found != null) {
if (found %s null) {
foundCount++;
}
});
if (foundCount === elements.length) {
return true;
}
@ -598,6 +613,7 @@ func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector
`,
eval.ParamString(selector.String()),
eval.ParamString(class.String()),
waitEventToEqOperator(when),
),
events.DefaultPolling,
)

View File

@ -695,7 +695,7 @@ func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector values.Str
return values.True
}
func (el *HTMLElement) WaitForClass(ctx context.Context, class values.String) error {
func (el *HTMLElement) WaitForClass(ctx context.Context, class values.String, when drivers.WaitEvent) error {
task := events.NewWaitTask(
func(ctx2 context.Context) (core.Value, error) {
current := el.GetAttribute(ctx2, "class")
@ -708,9 +708,28 @@ func (el *HTMLElement) WaitForClass(ctx context.Context, class values.String) er
classStr := string(class)
classes := strings.Split(string(str), " ")
for _, c := range classes {
if c == classStr {
return values.True, nil
if when != drivers.WaitEventAbsence {
for _, c := range classes {
if c == classStr {
// The value does not really matter if it's not None
// None indicates that operation needs to be repeated
return values.True, nil
}
}
} else {
var found values.Boolean
for _, c := range classes {
if c == classStr {
found = values.True
break
}
}
if found == values.False {
// The value does not really matter if it's not None
// None indicates that operation needs to be repeated
return values.False, nil
}
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"errors"
"github.com/MontFerret/ferret/pkg/drivers"
"math"
"strings"
@ -402,3 +403,11 @@ func createEventBroker(client *cdp.Client) (*events.EventBroker, error) {
return broker, nil
}
func waitEventToEqOperator(when drivers.WaitEvent) string {
if when == drivers.WaitEventAbsence {
return "=="
}
return "!="
}

View File

@ -225,15 +225,15 @@ func (doc *HTMLDocument) WaitForNavigation(_ context.Context) error {
return core.ErrNotSupported
}
func (doc *HTMLDocument) WaitForSelector(_ context.Context, _ values.String) error {
func (doc *HTMLDocument) WaitForElement(_ context.Context, _ values.String, _ drivers.WaitEvent) error {
return core.ErrNotSupported
}
func (doc *HTMLDocument) WaitForClassBySelector(_ context.Context, _, _ values.String) error {
func (doc *HTMLDocument) WaitForClassBySelector(_ context.Context, _, _ values.String, _ drivers.WaitEvent) error {
return core.ErrNotSupported
}
func (doc *HTMLDocument) WaitForClassBySelectorAll(_ context.Context, _, _ values.String) error {
func (doc *HTMLDocument) WaitForClassBySelectorAll(_ context.Context, _, _ values.String, _ drivers.WaitEvent) error {
return core.ErrNotSupported
}

View File

@ -309,7 +309,7 @@ func (nd *HTMLElement) Hover(_ context.Context) error {
return core.ErrNotSupported
}
func (nd *HTMLElement) WaitForClass(_ context.Context, _ values.String) error {
func (nd *HTMLElement) WaitForClass(_ context.Context, _ values.String, _ drivers.WaitEvent) error {
return core.ErrNotSupported
}

View File

@ -10,6 +10,9 @@ import (
)
type (
// WaitEvent is an enum that represents what event is needed to wait for
WaitEvent int
// Node is an interface from which a number of DOM API object types inherit.
// It allows those types to be treated similarly;
// for example, inheriting the same set of methods, or being tested in the same way.
@ -74,7 +77,7 @@ type (
Hover(ctx context.Context) error
WaitForClass(ctx context.Context, class values.String) error
WaitForClass(ctx context.Context, class values.String, when WaitEvent) error
}
// The Document interface represents any web page loaded in the browser
@ -120,10 +123,18 @@ type (
WaitForNavigation(ctx context.Context) error
WaitForSelector(ctx context.Context, selector values.String) error
WaitForElement(ctx context.Context, selector values.String, when WaitEvent) error
WaitForClassBySelector(ctx context.Context, selector, class values.String) error
WaitForClassBySelector(ctx context.Context, selector, class values.String, when WaitEvent) error
WaitForClassBySelectorAll(ctx context.Context, selector, class values.String) error
WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when WaitEvent) error
}
)
const (
// Event indicating to wait for value to appear
WaitEventPresence = 0
// Event indicating to wait for value to disappear
WaitEventAbsence = 1
)

View File

@ -14,36 +14,39 @@ const defaultTimeout = 5000
func NewLib() map[string]core.Function {
return map[string]core.Function{
"CLICK": Click,
"CLICK_ALL": ClickAll,
"DOCUMENT": Document,
"DOWNLOAD": Download,
"ELEMENT": Element,
"ELEMENT_EXISTS": ElementExists,
"ELEMENTS": Elements,
"ELEMENTS_COUNT": ElementsCount,
"HOVER": Hover,
"INNER_HTML": InnerHTML,
"INNER_HTML_ALL": InnerHTMLAll,
"INNER_TEXT": InnerText,
"INNER_TEXT_ALL": InnerTextAll,
"INPUT": Input,
"MOUSE": MouseMoveXY,
"NAVIGATE": Navigate,
"NAVIGATE_BACK": NavigateBack,
"NAVIGATE_FORWARD": NavigateForward,
"PAGINATION": Pagination,
"PDF": PDF,
"SCREENSHOT": Screenshot,
"SCROLL": ScrollXY,
"SCROLL_BOTTOM": ScrollBottom,
"SCROLL_ELEMENT": ScrollInto,
"SCROLL_TOP": ScrollTop,
"SELECT": Select,
"WAIT_ELEMENT": WaitElement,
"WAIT_CLASS": WaitClass,
"WAIT_CLASS_ALL": WaitClassAll,
"WAIT_NAVIGATION": WaitNavigation,
"CLICK": Click,
"CLICK_ALL": ClickAll,
"DOCUMENT": Document,
"DOWNLOAD": Download,
"ELEMENT": Element,
"ELEMENT_EXISTS": ElementExists,
"ELEMENTS": Elements,
"ELEMENTS_COUNT": ElementsCount,
"HOVER": Hover,
"INNER_HTML": InnerHTML,
"INNER_HTML_ALL": InnerHTMLAll,
"INNER_TEXT": InnerText,
"INNER_TEXT_ALL": InnerTextAll,
"INPUT": Input,
"MOUSE": MouseMoveXY,
"NAVIGATE": Navigate,
"NAVIGATE_BACK": NavigateBack,
"NAVIGATE_FORWARD": NavigateForward,
"PAGINATION": Pagination,
"PDF": PDF,
"SCREENSHOT": Screenshot,
"SCROLL": ScrollXY,
"SCROLL_BOTTOM": ScrollBottom,
"SCROLL_ELEMENT": ScrollInto,
"SCROLL_TOP": ScrollTop,
"SELECT": Select,
"WAIT_ELEMENT": WaitElement,
"WAIT_NO_ELEMENT": WaitNoElement,
"WAIT_CLASS": WaitClass,
"WAIT_NO_CLASS": WaitNoClass,
"WAIT_CLASS_ALL": WaitClassAll,
"WAIT_NO_CLASS_ALL": WaitNoClassAll,
"WAIT_NAVIGATION": WaitNavigation,
}
}

View File

@ -18,6 +18,23 @@ import (
// @param timeout (Int, optional) - If document is passed, this param must represent timeout.
// Otherwise not passed.
func WaitClass(ctx context.Context, args ...core.Value) (core.Value, error) {
return waitClassWhen(ctx, args, drivers.WaitEventPresence)
}
// WaitClass waits for a class to disappear on a given element.
// Stops the execution until the navigation ends or operation times out.
// @param docOrEl (HTMLDocument|HTMLElement) - Target document or element.
// @param selectorOrClass (String) - If document is passed, this param must represent an element selector.
// Otherwise target class.
// @param classOrTimeout (String|Int, optional) - If document is passed, this param must represent target class name.
// Otherwise timeout.
// @param timeout (Int, optional) - If document is passed, this param must represent timeout.
// Otherwise not passed.
func WaitNoClass(ctx context.Context, args ...core.Value) (core.Value, error) {
return waitClassWhen(ctx, args, drivers.WaitEventAbsence)
}
func waitClassWhen(ctx context.Context, args []core.Value, when drivers.WaitEvent) (core.Value, error) {
err := core.ValidateArgs(args, 2, 4)
if err != nil {
@ -74,7 +91,7 @@ func WaitClass(ctx context.Context, args ...core.Value) (core.Value, error) {
ctx, fn := waitTimeout(ctx, timeout)
defer fn()
return values.None, doc.WaitForClassBySelector(ctx, selector, class)
return values.None, doc.WaitForClassBySelector(ctx, selector, class, when)
}
el := arg1.(drivers.HTMLElement)
@ -93,5 +110,5 @@ func WaitClass(ctx context.Context, args ...core.Value) (core.Value, error) {
ctx, fn := waitTimeout(ctx, timeout)
defer fn()
return values.None, el.WaitForClass(ctx, class)
return values.None, el.WaitForClass(ctx, class, when)
}

View File

@ -2,6 +2,7 @@ package html
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
@ -14,6 +15,20 @@ import (
// @param class (String) - String of target CSS class.
// @param timeout (Int, optional) - Optional timeout.
func WaitClassAll(ctx context.Context, args ...core.Value) (core.Value, error) {
return waitClassAllWhen(ctx, args, drivers.WaitEventPresence)
}
// WaitClassAll waits for a class to disappear on all matched elements.
// Stops the execution until the navigation ends or operation times out.
// @param doc (HTMLDocument) - Parent document.
// @param selector (String) - String of CSS selector.
// @param class (String) - String of target CSS class.
// @param timeout (Int, optional) - Optional timeout.
func WaitNoClassAll(ctx context.Context, args ...core.Value) (core.Value, error) {
return waitClassAllWhen(ctx, args, drivers.WaitEventAbsence)
}
func waitClassAllWhen(ctx context.Context, args []core.Value, when drivers.WaitEvent) (core.Value, error) {
err := core.ValidateArgs(args, 3, 4)
if err != nil {
@ -57,5 +72,5 @@ func WaitClassAll(ctx context.Context, args ...core.Value) (core.Value, error) {
ctx, fn := waitTimeout(ctx, timeout)
defer fn()
return values.None, doc.WaitForClassBySelectorAll(ctx, selector, class)
return values.None, doc.WaitForClassBySelectorAll(ctx, selector, class, when)
}

View File

@ -2,6 +2,7 @@ package html
import (
"context"
"github.com/MontFerret/ferret/pkg/drivers"
"github.com/MontFerret/ferret/pkg/runtime/core"
"github.com/MontFerret/ferret/pkg/runtime/values"
"github.com/MontFerret/ferret/pkg/runtime/values/types"
@ -13,6 +14,19 @@ import (
// @param selector (String) - Target element's selector.
// @param timeout (Int, optional) - Optional timeout. Default 5000 ms.
func WaitElement(ctx context.Context, args ...core.Value) (core.Value, error) {
return waitElementWhen(ctx, args, drivers.WaitEventPresence)
}
// WaitNoElements waits for element to disappear in the DOM.
// Stops the execution until it does not find an element or operation times out.
// @param doc (HTMLDocument) - Driver HTMLDocument.
// @param selector (String) - Target element's selector.
// @param timeout (Int, optional) - Optional timeout. Default 5000 ms.
func WaitNoElement(ctx context.Context, args ...core.Value) (core.Value, error) {
return waitElementWhen(ctx, args, drivers.WaitEventAbsence)
}
func waitElementWhen(ctx context.Context, args []core.Value, when drivers.WaitEvent) (core.Value, error) {
err := core.ValidateArgs(args, 2, 3)
if err != nil {
@ -41,5 +55,5 @@ func WaitElement(ctx context.Context, args ...core.Value) (core.Value, error) {
ctx, fn := waitTimeout(ctx, timeout)
defer fn()
return values.None, doc.WaitForSelector(ctx, values.NewString(selector))
return values.None, doc.WaitForElement(ctx, values.NewString(selector), when)
}