mirror of
https://github.com/MontFerret/ferret.git
synced 2025-01-26 03:51:57 +02:00
Integration tests (#170)
This commit is contained in:
parent
423d84cd07
commit
de774ba03e
@ -1,5 +1,14 @@
|
||||
## Changelog
|
||||
|
||||
### 0.5.0
|
||||
#### Added
|
||||
- DateTime functions.
|
||||
|
||||
#### Fixed
|
||||
- Unable to define variables and make function calls before FILTER, SORT and etc statements.
|
||||
- ``INNER_HTML`` returns outer HTML instead for dynamic elements.
|
||||
- ``INNER_TEXT`` returns HTML instead from dynamic elements.
|
||||
|
||||
### 0.4.0
|
||||
#### Added
|
||||
- ``COLLECT`` keyword [#141](https://github.com/MontFerret/ferret/pull/141)
|
||||
|
49
e2e/main.go
49
e2e/main.go
@ -1,14 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/MontFerret/ferret/e2e/runner"
|
||||
"github.com/MontFerret/ferret/e2e/server"
|
||||
"github.com/rs/zerolog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -24,12 +24,6 @@ var (
|
||||
"root directory with test pages",
|
||||
)
|
||||
|
||||
port = flag.Uint64(
|
||||
"port",
|
||||
8080,
|
||||
"server port",
|
||||
)
|
||||
|
||||
cdp = flag.String(
|
||||
"cdp",
|
||||
"http://0.0.0.0:9222",
|
||||
@ -42,17 +36,27 @@ func main() {
|
||||
|
||||
logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
s := server.New(server.Settings{
|
||||
Port: *port,
|
||||
Dir: *pagesDir,
|
||||
staticPort := uint64(8080)
|
||||
static := server.New(server.Settings{
|
||||
Port: staticPort,
|
||||
Dir: filepath.Join(*pagesDir, "static"),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
dynamicPort := uint64(8081)
|
||||
dynamic := server.New(server.Settings{
|
||||
Port: dynamicPort,
|
||||
Dir: filepath.Join(*pagesDir, "dynamic"),
|
||||
})
|
||||
|
||||
go func() {
|
||||
if err := s.Start(); err != nil {
|
||||
logger.Info().Timestamp().Msg("shutting down the server")
|
||||
if err := static.Start(); err != nil {
|
||||
logger.Info().Timestamp().Msg("shutting down the static pages server")
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := dynamic.Start(); err != nil {
|
||||
logger.Info().Timestamp().Msg("shutting down the dynamic pages server")
|
||||
}
|
||||
}()
|
||||
|
||||
@ -71,18 +75,17 @@ func main() {
|
||||
}
|
||||
|
||||
r := runner.New(logger, runner.Settings{
|
||||
ServerAddress: fmt.Sprintf("http://0.0.0.0:%d", *port),
|
||||
CDPAddress: *cdp,
|
||||
Dir: *testsDir,
|
||||
StaticServerAddress: fmt.Sprintf("http://0.0.0.0:%d", staticPort),
|
||||
DynamicServerAddress: fmt.Sprintf("http://0.0.0.0:%d", dynamicPort),
|
||||
CDPAddress: *cdp,
|
||||
Dir: *testsDir,
|
||||
})
|
||||
|
||||
err := r.Run()
|
||||
|
||||
if err := s.Stop(ctx); err != nil {
|
||||
logger.Fatal().Timestamp().Err(err).Msg("failed to stop server")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
|
29
e2e/pages/dynamic/components/app.js
Normal file
29
e2e/pages/dynamic/components/app.js
Normal file
@ -0,0 +1,29 @@
|
||||
import Layout from './layout.js';
|
||||
import IndexPage from './pages/index.js';
|
||||
import FormsPage from './pages/forms/index.js';
|
||||
|
||||
const e = React.createElement;
|
||||
const Router = ReactRouter.Router;
|
||||
const Switch = ReactRouter.Switch;
|
||||
const Route = ReactRouter.Route;
|
||||
const Redirect = ReactRouter.Redirect;
|
||||
const createBrowserHistory = History.createBrowserHistory;
|
||||
|
||||
export default function AppComponent({ redirect = null}) {
|
||||
return e(Router, { history: createBrowserHistory() },
|
||||
e(Layout, null, [
|
||||
e(Switch, null, [
|
||||
e(Route, {
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: IndexPage
|
||||
}),
|
||||
e(Route, {
|
||||
path: '/forms',
|
||||
component: FormsPage
|
||||
})
|
||||
]),
|
||||
redirect ? e(Redirect, { to: redirect }) : null
|
||||
])
|
||||
)
|
||||
}
|
27
e2e/pages/dynamic/components/layout.js
Normal file
27
e2e/pages/dynamic/components/layout.js
Normal file
@ -0,0 +1,27 @@
|
||||
const e = React.createElement;
|
||||
const NavLink = ReactRouterDOM.NavLink;
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return e("div", { id: "layout"}, [
|
||||
e("nav", { className: "navbar navbar-expand-md navbar-dark bg-dark mb-4" }, [
|
||||
e(NavLink, { className: "navbar-brand", to: "/"}, "Ferret"),
|
||||
e("button", { className: "navbar-toggler", type: "button"}, [
|
||||
e("span", { className: "navbar-toggler-icon" })
|
||||
]),
|
||||
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("li", { className: "nav-item"}, [
|
||||
e(NavLink, { className: "nav-link", to: "/navigation" }, "Navigation")
|
||||
]),
|
||||
e("li", { className: "nav-item"}, [
|
||||
e(NavLink, { className: "nav-link", to: "/events" }, "Events")
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
e("main", { className: "container"}, children)
|
||||
])
|
||||
}
|
124
e2e/pages/dynamic/components/pages/forms/index.js
Normal file
124
e2e/pages/dynamic/components/pages/forms/index.js
Normal file
@ -0,0 +1,124 @@
|
||||
const e = React.createElement;
|
||||
|
||||
export default class FormsPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
textInput: "",
|
||||
select: "",
|
||||
multiSelect: "",
|
||||
textarea: ""
|
||||
};
|
||||
|
||||
this.handleTextInput = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
textInput: evt.target.value
|
||||
});
|
||||
};
|
||||
|
||||
this.handleSelect = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
select: evt.target.value
|
||||
});
|
||||
};
|
||||
|
||||
this.handleMultiSelect = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
multiSelect: Array.prototype.map.call(evt.target.selectedOptions, i => i.value).join(", ")
|
||||
});
|
||||
};
|
||||
|
||||
this.handleTtextarea = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
this.setState({
|
||||
textarea: evt.target.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return e("form", null, [
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Text input"),
|
||||
e("input", {
|
||||
id: "text_input",
|
||||
type: "text",
|
||||
className: "form-control",
|
||||
onChange: this.handleTextInput
|
||||
}),
|
||||
e("small", {
|
||||
id: "text_output",
|
||||
className: "form-text text-muted"
|
||||
},
|
||||
this.state.textInput
|
||||
)
|
||||
]),
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Select"),
|
||||
e("select", {
|
||||
id: "select_input",
|
||||
className: "form-control",
|
||||
onChange: this.handleSelect
|
||||
},
|
||||
[
|
||||
e("option", null, 1),
|
||||
e("option", null, 2),
|
||||
e("option", null, 3),
|
||||
e("option", null, 4),
|
||||
e("option", null, 5),
|
||||
]
|
||||
),
|
||||
e("small", {
|
||||
id: "select_output",
|
||||
className: "form-text text-muted"
|
||||
}, this.state.select
|
||||
)
|
||||
]),
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Multi select"),
|
||||
e("select", {
|
||||
id: "multi_select_input",
|
||||
multiple: true,
|
||||
className: "form-control",
|
||||
onChange: this.handleMultiSelect
|
||||
},
|
||||
[
|
||||
e("option", null, 1),
|
||||
e("option", null, 2),
|
||||
e("option", null, 3),
|
||||
e("option", null, 4),
|
||||
e("option", null, 5),
|
||||
]
|
||||
),
|
||||
e("small", {
|
||||
id: "multi_select_output",
|
||||
className: "form-text text-muted"
|
||||
}, this.state.multiSelect
|
||||
)
|
||||
]),
|
||||
e("div", { className: "form-group" }, [
|
||||
e("label", null, "Textarea"),
|
||||
e("textarea", {
|
||||
id: "textarea_input",
|
||||
rows:"5",
|
||||
className: "form-control",
|
||||
onChange: this.handleTtextarea
|
||||
}
|
||||
),
|
||||
e("small", {
|
||||
id: "textarea_output",
|
||||
className: "form-text text-muted"
|
||||
}, this.state.textarea
|
||||
)
|
||||
]),
|
||||
])
|
||||
}
|
||||
}
|
8
e2e/pages/dynamic/components/pages/index.js
Normal file
8
e2e/pages/dynamic/components/pages/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
const e = React.createElement;
|
||||
|
||||
export default function IndexPage() {
|
||||
return e("div", { className: "jumbotron" }, [
|
||||
e("div", null, e("h1", null, "Welcome to Ferret E2E test page!")),
|
||||
e("div", null, e("p", { className: "lead" }, "It has several pages for testing different possibilities of the library"))
|
||||
])
|
||||
}
|
4
e2e/pages/dynamic/index.css
Normal file
4
e2e/pages/dynamic/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
/* Show it's not fixed to the top */
|
||||
body {
|
||||
min-height: 75rem;
|
||||
}
|
21
e2e/pages/dynamic/index.html
Normal file
21
e2e/pages/dynamic/index.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Ferret E2E SPA</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="index.css">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div id="root"></div>
|
||||
<script src="https://unpkg.com/react@16.6.1/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@16.6.1/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/history@4.7.2/umd/history.min.js"></script>
|
||||
<script src="https://unpkg.com/react-router@4.3.1/umd/react-router.js"></script>
|
||||
<script src="https://unpkg.com/react-router-dom@4.3.1/umd/react-router-dom.js"></script>
|
||||
<script src="index.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
9
e2e/pages/dynamic/index.js
Normal file
9
e2e/pages/dynamic/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import AppComponent from "./components/app.js";
|
||||
import { parse } from "./utils/qs.js";
|
||||
|
||||
const qs = parse(location.search);
|
||||
|
||||
ReactDOM.render(
|
||||
React.createElement(AppComponent, qs),
|
||||
document.getElementById("root")
|
||||
);
|
82
e2e/pages/dynamic/utils/qs.js
Normal file
82
e2e/pages/dynamic/utils/qs.js
Normal file
@ -0,0 +1,82 @@
|
||||
'use strict';
|
||||
|
||||
var has = Object.prototype.hasOwnProperty
|
||||
, undef;
|
||||
|
||||
/**
|
||||
* Decode a URI encoded string.
|
||||
*
|
||||
* @param {String} input The URI encoded string.
|
||||
* @returns {String} The decoded string.
|
||||
* @api private
|
||||
*/
|
||||
function decode(input) {
|
||||
return decodeURIComponent(input.replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple query string parser.
|
||||
*
|
||||
* @param {String} query The query string that needs to be parsed.
|
||||
* @returns {Object}
|
||||
* @api public
|
||||
*/
|
||||
export function parse(query) {
|
||||
var parser = /([^=?&]+)=?([^&]*)/g
|
||||
, result = {}
|
||||
, part;
|
||||
|
||||
while (part = parser.exec(query)) {
|
||||
var key = decode(part[1])
|
||||
, value = decode(part[2]);
|
||||
|
||||
//
|
||||
// Prevent overriding of existing properties. This ensures that build-in
|
||||
// methods like `toString` or __proto__ are not overriden by malicious
|
||||
// querystrings.
|
||||
//
|
||||
if (key in result) continue;
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a query string to an object.
|
||||
*
|
||||
* @param {Object} obj Object that should be transformed.
|
||||
* @param {String} prefix Optional prefix.
|
||||
* @returns {String}
|
||||
* @api public
|
||||
*/
|
||||
export function stringify(obj, prefix) {
|
||||
prefix = prefix || '';
|
||||
|
||||
var pairs = []
|
||||
, value
|
||||
, key;
|
||||
|
||||
//
|
||||
// Optionally prefix with a '?' if needed
|
||||
//
|
||||
if ('string' !== typeof prefix) prefix = '?';
|
||||
|
||||
for (key in obj) {
|
||||
if (has.call(obj, key)) {
|
||||
value = obj[key];
|
||||
|
||||
//
|
||||
// Edge cases where we actually want to encode the value to an empty
|
||||
// string instead of the stringified value.
|
||||
//
|
||||
if (!value && (value === null || value === undef || isNaN(value))) {
|
||||
value = '';
|
||||
}
|
||||
|
||||
pairs.push(encodeURIComponent(key) +'='+ encodeURIComponent(value));
|
||||
}
|
||||
}
|
||||
|
||||
return pairs.length ? prefix + pairs.join('&') : '';
|
||||
}
|
@ -14,9 +14,10 @@ import (
|
||||
|
||||
type (
|
||||
Settings struct {
|
||||
ServerAddress string
|
||||
CDPAddress string
|
||||
Dir string
|
||||
StaticServerAddress string
|
||||
DynamicServerAddress string
|
||||
CDPAddress string
|
||||
Dir string
|
||||
}
|
||||
|
||||
Result struct {
|
||||
@ -129,7 +130,8 @@ func (r *Runner) runQuery(c *compiler.FqlCompiler, name, script string) Result {
|
||||
out, err := p.Run(
|
||||
context.Background(),
|
||||
runtime.WithBrowser(r.settings.CDPAddress),
|
||||
runtime.WithParam("server", r.settings.ServerAddress),
|
||||
runtime.WithParam("static", r.settings.StaticServerAddress),
|
||||
runtime.WithParam("dynamic", r.settings.DynamicServerAddress),
|
||||
)
|
||||
|
||||
duration := time.Now().Sub(start)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/labstack/echo"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -23,6 +24,7 @@ func New(settings Settings) *Server {
|
||||
e.HideBanner = true
|
||||
|
||||
e.Static("/", settings.Dir)
|
||||
e.File("/", filepath.Join(settings.Dir, "index.html"))
|
||||
|
||||
return &Server{e, settings}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = '<li class="toc-entry toc-h2"><a href="#containers">Containers</a></li><li class="toc-entry toc-h2"><a href="#responsive-breakpoints">Responsive breakpoints</a></li><li class="toc-entry toc-h2"><a href="#z-index">Z-index</a></li>'
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = [
|
12
e2e/tests/doc_inner_html_all_d.fql
Normal file
12
e2e/tests/doc_inner_html_all_d.fql
Normal file
@ -0,0 +1,12 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = [
|
||||
'<h1>Welcome to Ferret E2E test page!</h1>',
|
||||
'<p class="lead">It has several pages for testing different possibilities of the library</p>'
|
||||
]
|
||||
LET actual = INNER_HTML_ALL(doc, '#root > div > main > div > *')
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
10
e2e/tests/doc_inner_html_d.fql
Normal file
10
e2e/tests/doc_inner_html_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = '#root > div > main > div'
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = '<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>'
|
||||
LET actual = INNER_HTML(doc, selector)
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, '\s', ''), REGEXP_REPLACE(TRIM(actual), '(\n|\s)', ''))
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = "Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes."
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/grid.html'
|
||||
LET url = @static + '/grid.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
LET expected = [
|
16
e2e/tests/doc_inner_text_all_d.fql
Normal file
16
e2e/tests/doc_inner_text_all_d.fql
Normal file
@ -0,0 +1,16 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = '#root > div > main > div > *'
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = [
|
||||
'Welcome to Ferret E2E test page!',
|
||||
'It has several pages for testing different possibilities of the library'
|
||||
]
|
||||
LET actual = (
|
||||
FOR str IN INNER_TEXT_ALL(doc, selector)
|
||||
RETURN REGEXP_REPLACE(TRIM(str), '\n', '')
|
||||
)
|
||||
|
||||
RETURN EXPECT(expected, actual)
|
10
e2e/tests/doc_inner_text_d.fql
Normal file
10
e2e/tests/doc_inner_text_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
LET selector = '#root > div > main > div h1'
|
||||
|
||||
WAIT_ELEMENT(doc, "#layout")
|
||||
|
||||
LET expected = 'Welcome to Ferret E2E test page!'
|
||||
LET actual = INNER_TEXT(doc, selector)
|
||||
|
||||
RETURN EXPECT(REGEXP_REPLACE(expected, '\s', ''), REGEXP_REPLACE(TRIM(actual), '(\n|\s)', ''))
|
10
e2e/tests/doc_input_text_d.fql
Normal file
10
e2e/tests/doc_input_text_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET output = ELEMENT(doc, "#text_output")
|
||||
|
||||
INPUT(doc, "#text_input", "foo")
|
||||
|
||||
RETURN EXPECT(output.innerText, "foo")
|
9
e2e/tests/doc_select_multi_d.fql
Normal file
9
e2e/tests/doc_select_multi_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET output = ELEMENT(doc, "#multi_select_output")
|
||||
LET result = SELECT(doc, "#multi_select_input", ["1", "2", "4"])
|
||||
|
||||
RETURN EXPECT(output.innerText, "1, 2, 4") + EXPECT(JSON_STRINGIFY(result), '["1","2","4"]')
|
9
e2e/tests/doc_select_single_d.fql
Normal file
9
e2e/tests/doc_select_single_d.fql
Normal file
@ -0,0 +1,9 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET output = ELEMENT(doc, "#select_output")
|
||||
LET result = SELECT(doc, "#select_input", ["4"])
|
||||
|
||||
RETURN EXPECT(output.innerText, "4") + EXPECT(JSON_STRINGIFY(result), '["4"]')
|
11
e2e/tests/el_input_text_d.fql
Normal file
11
e2e/tests/el_input_text_d.fql
Normal file
@ -0,0 +1,11 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET input = ELEMENT(doc, "#text_input")
|
||||
LET output = ELEMENT(doc, "#text_output")
|
||||
|
||||
INPUT(input, "foo")
|
||||
|
||||
RETURN EXPECT(output.innerText, "foo")
|
10
e2e/tests/el_select_multi_d.fql
Normal file
10
e2e/tests/el_select_multi_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET input = ELEMENT(doc, "#multi_select_input")
|
||||
LET output = ELEMENT(doc, "#multi_select_output")
|
||||
LET result = SELECT(input, ["1", "2", "4"])
|
||||
|
||||
RETURN EXPECT(output.innerText, "1, 2, 4") + EXPECT(JSON_STRINGIFY(result), '["1","2","4"]')
|
10
e2e/tests/el_select_single_d.fql
Normal file
10
e2e/tests/el_select_single_d.fql
Normal file
@ -0,0 +1,10 @@
|
||||
LET url = @dynamic + "?redirect=/forms"
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
WAIT_ELEMENT(doc, "form")
|
||||
|
||||
LET input = ELEMENT(doc, "#select_input")
|
||||
LET output = ELEMENT(doc, "#select_output")
|
||||
LET result = SELECT(input, ["4"])
|
||||
|
||||
RETURN EXPECT(output.innerText, "4") + EXPECT(JSON_STRINGIFY(result), '["4"]')
|
@ -1,10 +0,0 @@
|
||||
// LET url = @server + '/bootstrap/overview.html'
|
||||
// LET doc = DOCUMENT(url, true)
|
||||
// LET selector = '.section-nav'
|
||||
|
||||
// LET expected = '<li class="toc-entry toc-h2"><a href="#containers">Containers</a></li><li class="toc-entry toc-h2"><a href="#responsive-breakpoints">Responsive breakpoints</a></li><li class="toc-entry toc-h2"><a href="#z-index">Z-index</a></li>'
|
||||
// LET actual = INNER_HTML(doc, selector)
|
||||
|
||||
// RETURN EXPECT(REGEXP_REPLACE(expected, '\s', ''), REGEXP_REPLACE(TRIM(actual), '(\n|\s)', ''))
|
||||
|
||||
RETURN ""
|
@ -1,9 +0,0 @@
|
||||
// LET url = @server + '/bootstrap/overview.html'
|
||||
// LET doc = DOCUMENT(url, true)
|
||||
// LET selector = 'body > div > div > main > p.bd-lead'
|
||||
|
||||
// LET expected = "Components and options for laying out your Bootstrap project, including wrapping containers, a powerful grid system, a flexible media object, and responsive utility classes."
|
||||
// LET actual = INNER_TEXT(doc, selector)
|
||||
|
||||
// RETURN EXPECT(expected, actual)
|
||||
RETURN ""
|
@ -1,4 +1,4 @@
|
||||
LET url = @server + '/bootstrap/overview.html'
|
||||
LET url = @static + '/overview.html'
|
||||
LET doc = DOCUMENT(url)
|
||||
|
||||
RETURN EXPECT(doc.url, url)
|
4
e2e/tests/page_load_d.fql
Normal file
4
e2e/tests/page_load_d.fql
Normal file
@ -0,0 +1,4 @@
|
||||
LET url = @dynamic
|
||||
LET doc = DOCUMENT(url, true)
|
||||
|
||||
RETURN EXPECT(doc.url, url)
|
@ -356,7 +356,7 @@ func (doc *HTMLDocument) InnerTextBySelector(selector values.String) values.Stri
|
||||
doc.Lock()
|
||||
defer doc.Unlock()
|
||||
|
||||
return doc.element.InnerHTMLBySelector(selector)
|
||||
return doc.element.InnerTextBySelector(selector)
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) InnerTextBySelectorAll(selector values.String) *values.Array {
|
||||
@ -479,6 +479,58 @@ func (doc *HTMLDocument) InputBySelector(selector values.String, value core.Valu
|
||||
return values.True, nil
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) SelectBySelector(selector values.String, value *values.Array) (*values.Array, error) {
|
||||
res, err := eval.Eval(
|
||||
doc.client,
|
||||
fmt.Sprintf(`
|
||||
var element = document.querySelector(%s);
|
||||
|
||||
if (element == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var values = %s;
|
||||
|
||||
if (element.nodeName.toLowerCase() !== 'select') {
|
||||
throw new Error('Element is not a <select> element.');
|
||||
}
|
||||
|
||||
var options = Array.from(element.options);
|
||||
element.value = undefined;
|
||||
|
||||
for (var option of options) {
|
||||
option.selected = values.includes(option.value);
|
||||
|
||||
if (option.selected && !element.multiple) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
|
||||
return options.filter(option => option.selected).map(option => option.value);
|
||||
`,
|
||||
eval.ParamString(selector.String()),
|
||||
value.String(),
|
||||
),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr, ok := res.(*values.Array)
|
||||
|
||||
if ok {
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
return nil, core.TypeError(core.ArrayType, res.Type())
|
||||
}
|
||||
|
||||
func (doc *HTMLDocument) WaitForSelector(selector values.String, timeout values.Int) error {
|
||||
task := events.NewEvalWaitTask(
|
||||
doc.client,
|
||||
@ -702,6 +754,7 @@ func (doc *HTMLDocument) PrintToPDF(params *page.PrintToPDFArgs) (core.Value, er
|
||||
ctx := context.Background()
|
||||
|
||||
reply, err := doc.client.Page.PrintToPDF(ctx, params)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gofrs/uuid"
|
||||
"hash/fnv"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -27,6 +28,7 @@ const DefaultTimeout = time.Second * 30
|
||||
var emptyNodeID = dom.NodeID(0)
|
||||
var emptyBackendID = dom.BackendNodeID(0)
|
||||
var emptyObjectID = ""
|
||||
var attrID = "data-ferret-id"
|
||||
|
||||
type (
|
||||
HTMLElementIdentity struct {
|
||||
@ -746,6 +748,78 @@ func (el *HTMLElement) Input(value core.Value, delay values.Int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (el *HTMLElement) Select(value *values.Array) (*values.Array, error) {
|
||||
if el.NodeName() != "SELECT" {
|
||||
return nil, core.Error(core.ErrInvalidOperation, "Element is not a <select> element.")
|
||||
}
|
||||
|
||||
id, err := uuid.NewV4()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := contextWithTimeout()
|
||||
defer cancel()
|
||||
|
||||
err = el.client.DOM.SetAttributeValue(ctx, dom.NewSetAttributeValueArgs(el.id.nodeID, attrID, id.String()))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := eval.Eval(
|
||||
el.client,
|
||||
fmt.Sprintf(`
|
||||
var element = document.querySelector('[%s="%s"]');
|
||||
|
||||
if (element == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var values = %s;
|
||||
|
||||
if (element.nodeName.toLowerCase() !== 'select') {
|
||||
throw new Error('Element is not a <select> element.');
|
||||
}
|
||||
|
||||
var options = Array.from(element.options);
|
||||
element.value = undefined;
|
||||
|
||||
for (var option of options) {
|
||||
option.selected = values.includes(option.value);
|
||||
|
||||
if (option.selected && !element.multiple) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
element.dispatchEvent(new Event('input', { 'bubbles': true }));
|
||||
element.dispatchEvent(new Event('change', { 'bubbles': true }));
|
||||
|
||||
return options.filter(option => option.selected).map(option => option.value);
|
||||
`,
|
||||
attrID,
|
||||
id.String(),
|
||||
value.String(),
|
||||
),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr, ok := res.(*values.Array)
|
||||
|
||||
if ok {
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
return nil, core.TypeError(core.ArrayType, res.Type())
|
||||
}
|
||||
|
||||
func (el *HTMLElement) IsConnected() values.Boolean {
|
||||
el.mu.Lock()
|
||||
defer el.mu.Unlock()
|
||||
|
@ -3,13 +3,16 @@ package dynamic
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/MontFerret/ferret/pkg/html/common"
|
||||
"github.com/MontFerret/ferret/pkg/html/dynamic/eval"
|
||||
"github.com/MontFerret/ferret/pkg/html/dynamic/events"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/mafredri/cdp"
|
||||
"github.com/mafredri/cdp/protocol/dom"
|
||||
"github.com/mafredri/cdp/protocol/page"
|
||||
"github.com/mafredri/cdp/protocol/runtime"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"strings"
|
||||
)
|
||||
@ -65,23 +68,43 @@ func parseAttrs(attrs []string) *values.Object {
|
||||
}
|
||||
|
||||
func loadInnerHTML(ctx context.Context, client *cdp.Client, id *HTMLElementIdentity) (values.String, error) {
|
||||
var args *dom.GetOuterHTMLArgs
|
||||
var objID runtime.RemoteObjectID
|
||||
|
||||
if id.objectID != "" {
|
||||
args = dom.NewGetOuterHTMLArgs().SetObjectID(id.objectID)
|
||||
objID = id.objectID
|
||||
} else if id.backendID > 0 {
|
||||
args = dom.NewGetOuterHTMLArgs().SetBackendNodeID(id.backendID)
|
||||
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetBackendNodeID(id.backendID))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if repl.Object.ObjectID == nil {
|
||||
return "", errors.New("unable to resolve node")
|
||||
}
|
||||
|
||||
objID = *repl.Object.ObjectID
|
||||
} else {
|
||||
args = dom.NewGetOuterHTMLArgs().SetNodeID(id.nodeID)
|
||||
repl, err := client.DOM.ResolveNode(ctx, dom.NewResolveNodeArgs().SetNodeID(id.nodeID))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if repl.Object.ObjectID == nil {
|
||||
return "", errors.New("unable to resolve node")
|
||||
}
|
||||
|
||||
objID = *repl.Object.ObjectID
|
||||
}
|
||||
|
||||
res, err := client.DOM.GetOuterHTML(ctx, args)
|
||||
res, err := eval.Property(ctx, client, objID, "innerHTML")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return values.NewString(res.OuterHTML), err
|
||||
return values.NewString(res.String()), err
|
||||
}
|
||||
|
||||
func parseInnerText(innerHTML string) (values.String, error) {
|
||||
|
@ -30,7 +30,6 @@ func Input(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||
|
||||
switch args[0].(type) {
|
||||
case *dynamic.HTMLDocument:
|
||||
|
||||
doc, ok := arg1.(*dynamic.HTMLDocument)
|
||||
|
||||
if !ok {
|
||||
@ -46,6 +45,7 @@ func Input(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||
}
|
||||
|
||||
delay := values.Int(0)
|
||||
|
||||
if len(args) == 4 {
|
||||
arg4 := args[3]
|
||||
|
||||
@ -58,7 +58,6 @@ func Input(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||
}
|
||||
|
||||
return doc.InputBySelector(arg2.(values.String), args[2], delay)
|
||||
|
||||
case *dynamic.HTMLElement:
|
||||
el, ok := arg1.(*dynamic.HTMLElement)
|
||||
|
||||
@ -67,6 +66,7 @@ func Input(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||
}
|
||||
|
||||
delay := values.Int(0)
|
||||
|
||||
if len(args) == 3 {
|
||||
arg3 := args[2]
|
||||
|
||||
|
@ -32,6 +32,7 @@ func NewLib() map[string]core.Function {
|
||||
"INNER_HTML_ALL": InnerHTMLAll,
|
||||
"INNER_TEXT": InnerText,
|
||||
"INNER_TEXT_ALL": InnerTextAll,
|
||||
"SELECT": Select,
|
||||
"SCREENSHOT": Screenshot,
|
||||
"PDF": PDF,
|
||||
"DOWNLOAD": Download,
|
||||
|
72
pkg/stdlib/html/select.go
Normal file
72
pkg/stdlib/html/select.go
Normal file
@ -0,0 +1,72 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/MontFerret/ferret/pkg/html/dynamic"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/core"
|
||||
"github.com/MontFerret/ferret/pkg/runtime/values"
|
||||
)
|
||||
|
||||
// Select selects a value from an underlying select element.
|
||||
// @param source (Document | Element) - Event target.
|
||||
// @param valueOrSelector (String | Array<String>) - Selector or a an array of strings as a value.
|
||||
// @param value (Array<String) - Target value. Optional.
|
||||
// @returns (Array<String>) - Returns an array of selected values.
|
||||
func Select(_ context.Context, args ...core.Value) (core.Value, error) {
|
||||
err := core.ValidateArgs(args, 2, 4)
|
||||
|
||||
if err != nil {
|
||||
return values.None, err
|
||||
}
|
||||
|
||||
arg1 := args[0]
|
||||
err = core.ValidateType(arg1, core.HTMLDocumentType, core.HTMLElementType)
|
||||
|
||||
if err != nil {
|
||||
return values.False, err
|
||||
}
|
||||
|
||||
switch args[0].(type) {
|
||||
case *dynamic.HTMLDocument:
|
||||
doc, ok := arg1.(*dynamic.HTMLDocument)
|
||||
|
||||
if !ok {
|
||||
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
|
||||
}
|
||||
|
||||
// selector
|
||||
arg2 := args[1]
|
||||
err = core.ValidateType(arg2, core.StringType)
|
||||
|
||||
if err != nil {
|
||||
return values.False, err
|
||||
}
|
||||
|
||||
arg3 := args[2]
|
||||
err = core.ValidateType(arg3, core.ArrayType)
|
||||
|
||||
if err != nil {
|
||||
return values.False, err
|
||||
}
|
||||
|
||||
return doc.SelectBySelector(arg2.(values.String), arg3.(*values.Array))
|
||||
case *dynamic.HTMLElement:
|
||||
el, ok := arg1.(*dynamic.HTMLElement)
|
||||
|
||||
if !ok {
|
||||
return values.False, core.Errors(core.ErrInvalidType, ErrNotDynamic)
|
||||
}
|
||||
|
||||
arg2 := args[1]
|
||||
|
||||
err = core.ValidateType(arg2, core.ArrayType)
|
||||
|
||||
if err != nil {
|
||||
return values.False, err
|
||||
}
|
||||
|
||||
return el.Select(arg2.(*values.Array))
|
||||
default:
|
||||
return values.False, core.Errors(core.ErrInvalidArgument)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user