├── pkg
├── drivers
│ ├── cdp
│ │ ├── eval
│ │ │ ├── helpers_test.go
│ │ │ ├── value.go
│ │ │ ├── return.go
│ │ │ ├── arguments.go
│ │ │ └── helpers.go
│ │ ├── events
│ │ │ └── helpers.go
│ │ ├── templates
│ │ │ ├── url.go
│ │ │ ├── parent.go
│ │ │ ├── document.go
│ │ │ ├── siblings.go
│ │ │ ├── value.go
│ │ │ └── helpers.go
│ │ ├── input
│ │ │ └── helpers.go
│ │ ├── network
│ │ │ └── options.go
│ │ ├── utils
│ │ │ └── layout.go
│ │ └── dom
│ │ │ ├── helpers.go
│ │ │ └── loader.go
│ ├── events.go
│ ├── errors.go
│ ├── consts.go
│ └── common
│ │ ├── path.go
│ │ ├── ua.go
│ │ ├── errors.go
│ │ ├── types.go
│ │ └── frames.go
├── runtime
│ ├── core
│ │ ├── consts.go
│ │ ├── cloneable.go
│ │ ├── predicate.go
│ │ ├── setter.go
│ │ ├── source.go
│ │ ├── expression.go
│ │ ├── source_test.go
│ │ └── getter.go
│ ├── errors.go
│ ├── collections
│ │ ├── iterator.go
│ │ └── collection.go
│ ├── expressions
│ │ ├── literals
│ │ │ ├── int.go
│ │ │ ├── float.go
│ │ │ ├── string.go
│ │ │ ├── none.go
│ │ │ └── boolean.go
│ │ └── member_path.go
│ └── values
│ │ └── types
│ │ ├── types.go
│ │ └── helpers.go
├── stdlib
│ ├── testing
│ │ ├── base
│ │ │ ├── errors.go
│ │ │ └── format.go
│ │ ├── fail.go
│ │ ├── int.go
│ │ ├── array.go
│ │ ├── float.go
│ │ ├── binary.go
│ │ ├── object.go
│ │ ├── string.go
│ │ ├── datetime.go
│ │ ├── none.go
│ │ ├── true.go
│ │ ├── false.go
│ │ ├── equal.go
│ │ ├── lt.go
│ │ ├── gt.go
│ │ ├── lte.go
│ │ ├── fail_test.go
│ │ └── gte.go
│ ├── utils
│ │ └── lib.go
│ ├── collections
│ │ └── lib.go
│ ├── io
│ │ ├── fs
│ │ │ ├── util_test.go
│ │ │ └── lib.go
│ │ ├── net
│ │ │ ├── lib.go
│ │ │ └── http
│ │ │ │ ├── lib.go
│ │ │ │ ├── post.go
│ │ │ │ ├── put.go
│ │ │ │ └── delete.go
│ │ └── lib.go
│ ├── math
│ │ ├── pi_test.go
│ │ ├── rand_test.go
│ │ ├── pi.go
│ │ ├── sqrt_test.go
│ │ ├── acos_test.go
│ │ ├── degrees_test.go
│ │ ├── percentile_test.go
│ │ ├── variance_sample_test.go
│ │ ├── variance_population_test.go
│ │ ├── stddev_sample_test.go
│ │ ├── stddev_population_test.go
│ │ ├── tan.go
│ │ ├── sqrt.go
│ │ ├── cos.go
│ │ ├── exp2.go
│ │ ├── ceil_test.go
│ │ ├── mean.go
│ │ └── sin.go
│ ├── objects
│ │ └── lib.go
│ ├── datetime
│ │ ├── now.go
│ │ ├── now_test.go
│ │ ├── day.go
│ │ ├── hour.go
│ │ ├── second.go
│ │ ├── year.go
│ │ └── month.go
│ ├── path
│ │ └── lib.go
│ ├── arrays
│ │ ├── outersection.go
│ │ ├── first.go
│ │ └── last.go
│ ├── types
│ │ ├── type_name.go
│ │ ├── to_date_time.go
│ │ ├── to_string.go
│ │ ├── to_binary.go
│ │ ├── is_int.go
│ │ ├── is_none.go
│ │ ├── is_array.go
│ │ ├── is_float.go
│ │ ├── is_binary.go
│ │ ├── is_boolean.go
│ │ ├── is_object.go
│ │ ├── is_string.go
│ │ ├── to_binary_test.go
│ │ ├── is_date_time.go
│ │ ├── is_html_element.go
│ │ ├── is_html_document.go
│ │ └── is_nan.go
│ ├── html
│ │ ├── xpath_selector.go
│ │ ├── elements.go
│ │ ├── element_exists.go
│ │ └── elements_count.go
│ └── strings
│ │ ├── escape_test.go
│ │ ├── unescape_test.go
│ │ └── escape.go
└── compiler
│ ├── options.go
│ ├── fuzz
│ └── fuzz.go
│ ├── result.go
│ ├── errors.go
│ └── compiler_eq_test.go
├── e2e
├── pages
│ ├── dynamic
│ │ ├── components
│ │ │ └── pages
│ │ │ │ ├── events
│ │ │ │ └── countable.js
│ │ │ │ └── index.js
│ │ ├── index.css
│ │ ├── index.js
│ │ └── utils
│ │ │ └── random.js
│ └── static
│ │ └── simple.html
└── tests
│ ├── examples
│ ├── click.yaml
│ ├── value.yaml
│ ├── iframes.yaml
│ ├── navigate.yamlx
│ ├── dynamic-page.yaml
│ ├── static-page.yaml
│ ├── inner_text_all.yaml
│ ├── lazy-loading.yaml
│ ├── navigate_back.yaml
│ ├── navigate_back_2.yaml
│ ├── navigate_forward.yaml
│ ├── navigate_forward_2.yaml
│ ├── wait_request.yaml
│ ├── wait_response.yaml
│ ├── blank-page.yaml
│ ├── split-new-line.yaml
│ ├── google-search.yaml
│ ├── pagination.yaml
│ └── pagination_uncontrolled.yaml
│ ├── static
│ ├── load.fql
│ ├── doc
│ │ ├── xpath
│ │ │ ├── count.fql
│ │ │ ├── query.fql
│ │ │ └── attr.fql
│ │ ├── element
│ │ │ ├── exists_by_css.fql
│ │ │ └── exists_by_xpath.fql
│ │ ├── inner_text
│ │ │ ├── get_by_css.fql
│ │ │ ├── get_by_xpath.fql
│ │ │ └── get.fql
│ │ ├── cookies
│ │ │ ├── get.fqlx
│ │ │ ├── default.fqlx
│ │ │ └── override_default.fqlx
│ │ └── inner_html
│ │ │ ├── get_by_css_all.fql
│ │ │ ├── get_by_xpath_all.fql
│ │ │ ├── get.fql
│ │ │ ├── get_by_css.fql
│ │ │ └── get_by_xpath.fql
│ ├── element
│ │ ├── children
│ │ │ ├── count_by_css.fql
│ │ │ ├── count_by_xpath.fql
│ │ │ ├── get_by_index.fql
│ │ │ ├── get_by_css.fql
│ │ │ └── get_by_xpath.fql
│ │ ├── xpath
│ │ │ ├── count.fql
│ │ │ └── query.fql
│ │ ├── query
│ │ │ ├── exists_by_css.fql
│ │ │ └── exists_by_xpath.fql
│ │ ├── parent
│ │ │ └── get.fql
│ │ ├── attrs
│ │ │ ├── get_by_xpath.fql
│ │ │ └── get.fql
│ │ ├── inner_text
│ │ │ ├── get.fql
│ │ │ └── set.fql
│ │ ├── inner_html
│ │ │ ├── get.fql
│ │ │ └── set.fql
│ │ ├── siblings
│ │ │ ├── prev.fql
│ │ │ └── next.fql
│ │ └── value
│ │ │ └── get.fql
│ ├── parse.fql
│ ├── ua.fqlx
│ └── headers
│ │ ├── default.fqlx
│ │ ├── override_default.fqlx
│ │ └── set.fqlx
│ └── dynamic
│ ├── doc
│ ├── iframes
│ │ ├── length.fql
│ │ ├── lookup.fql
│ │ ├── element_exists.fql
│ │ ├── input.fql
│ │ ├── hover.fql
│ │ └── wait_class.fql
│ ├── xpath
│ │ ├── count.fql
│ │ ├── attr.fql
│ │ └── query.fql
│ ├── element
│ │ ├── exists_by_css.fql
│ │ └── exists_by_xpath.fql
│ ├── focus
│ │ ├── focus_by_css.fql
│ │ └── focus_by_xpath.fql
│ ├── click
│ │ ├── click.fql
│ │ ├── click_by_css.fql
│ │ ├── click_by_xpath.fql
│ │ ├── click_with_count.fql
│ │ ├── click_by_css_with_count.fql
│ │ └── click_by_xpath_with_count.fql
│ ├── pagination_by_css.fql
│ ├── input
│ │ ├── input_by_css.fql
│ │ └── input_by_xpath.fql
│ ├── waitfor_event
│ │ ├── navigation.fql
│ │ └── frame_navigation.fql
│ ├── pagination_by_xpath.fql
│ ├── wait
│ │ ├── class_all_by_css.fql
│ │ ├── attr_all_by_css.fql
│ │ ├── no_class_all_by_css.fql
│ │ ├── class_all_by_xpath.fql
│ │ ├── no_class_all_by_xpath.fql
│ │ ├── class_by_css.fql
│ │ ├── element_by_css.fql
│ │ ├── no_class_by_css.fql
│ │ ├── class_by_xpath.fql
│ │ ├── attr_all_by_xpath.fql
│ │ ├── no_element_by_css.fql
│ │ ├── style_by_css.fql
│ │ ├── element_by_xpath.fql
│ │ ├── no_element_by_xpath.fql
│ │ ├── style_by_xpath.fql
│ │ ├── no_class_by_xpath.fql
│ │ ├── frame_navigation.fql
│ │ ├── attr_by_css.fql
│ │ ├── no_attr_by_css.fql
│ │ ├── attr_by_xpath.fql
│ │ ├── no_attr_by_xpath.fql
│ │ ├── style_all_by_css.fql
│ │ ├── style_all_by_xpath.fql
│ │ ├── no_style_by_css.fql
│ │ └── no_style_by_xpath.fql
│ ├── select
│ │ ├── single_by_css.fql
│ │ ├── single_by_xpath.fql
│ │ ├── multi_by_css.fql
│ │ └── multi_by_xpath.fql
│ ├── inner_text
│ │ ├── set.fql
│ │ ├── get_by_css.fql
│ │ ├── get_by_xpath.fql
│ │ ├── get.fql
│ │ ├── get_by_css_all.fql
│ │ └── get_by_xpath_all.fql
│ ├── inner_html
│ │ ├── set.fql
│ │ ├── get_by_css_all.fql
│ │ ├── get_by_xpath_all.fql
│ │ ├── get_by_css.fql
│ │ └── get_by_xpath.fql
│ └── viewport
│ │ └── size.fql
│ ├── page
│ ├── navigate
│ │ └── navigate.fql
│ ├── load.fql
│ ├── cookies
│ │ ├── set.fql
│ │ ├── load_with.fql
│ │ ├── get.fqlx
│ │ ├── delete.fql
│ │ ├── default.fqlx
│ │ └── override_default.fqlx
│ ├── parse.fql
│ ├── url
│ │ └── get.fql
│ └── headers
│ │ ├── default.fqlx
│ │ ├── override_default.fqlx
│ │ └── set.fqlx
│ └── element
│ ├── inner_html
│ ├── get_by_css_not_found.fail.fql
│ ├── get_by_xpath_not_found.fail.fql
│ └── set.fql
│ ├── inner_text
│ ├── get_by_css_not_found.fail.fql
│ ├── set_by_css.fail.fql
│ ├── get_by_xpath_not_found.fail.fql
│ ├── set_by_xpath.fail.fql
│ ├── set.fql
│ └── get.fql
│ ├── query
│ ├── element_by_css.fql
│ ├── element_by_xpath.fql
│ ├── element_not_found_by_css.fql
│ ├── element_not_found_by_xpath.fql
│ ├── elements_by_css.fql
│ ├── elements_by_xpath.fql
│ ├── exists.fql
│ └── exists_by_xpath.fql
│ ├── xpath
│ ├── count.fql
│ ├── attrs.fql
│ └── query.fql
│ ├── children
│ ├── count.fql
│ ├── get_by_index.fql
│ └── get.fql
│ ├── focus
│ ├── focus_by_css.fql
│ ├── focus_by_xpath.fql
│ └── focus.fql
│ ├── siblings
│ ├── next_not_found.fql
│ ├── prev_not_found.fql
│ ├── next.fql
│ └── prev.fql
│ ├── parent
│ └── get.fql
│ ├── press
│ ├── press_by_css.fql
│ ├── press_by_xpath.fql
│ ├── press.fql
│ └── press_multi.fql
│ ├── value
│ └── get.fql
│ ├── click
│ ├── click.fql
│ ├── click_by_css.fql
│ ├── click_by_xpath.fql
│ ├── click_with_count.fql
│ ├── click_by_css_with_count.fql
│ └── click_by_xpath_with_count.fql
│ ├── input
│ ├── input.fql
│ ├── input_by_css.fql
│ ├── input_by_css_with_timeout.fql
│ ├── input_by_xpath.fql
│ └── input_by_xpath_with_timeout.fql
│ ├── blur
│ ├── blur_by_css.fql
│ ├── blur.fql
│ └── blur_by_xpath.fql
│ ├── hover
│ ├── hover_by_css.fql
│ ├── hover_by_xpath.fql
│ └── hover.fql
│ ├── attrs
│ ├── get.fql
│ ├── get_2.fql
│ ├── set_many.fql
│ ├── remove.fql
│ ├── set.fql
│ └── remove_style.fql
│ ├── select
│ ├── single.fql
│ └── multi.fql
│ ├── iframes
│ ├── input.fql
│ ├── select_single.fql
│ ├── hover.fql
│ └── wait_class.fql
│ ├── styles
│ ├── get.fql
│ ├── set_2.fql
│ ├── remove.fql
│ └── set_many.fql
│ ├── wait
│ ├── attr_2.fql
│ ├── no_style.fql
│ ├── class.fql
│ ├── no_class.fql
│ ├── attr.fql
│ ├── style.fql
│ └── no_attr.fql
│ └── clear
│ ├── clear.fql
│ ├── clear_by_css.fql
│ └── clear_by_xpath.fql
├── .editorconfig
├── examples
├── screenshot.fql
├── blank-page.fql
├── download.fql
├── query-all.fql
├── xpath.fql
├── value.fql
├── split-new-line.fql
├── headers.fql
├── redirects.fql
├── click.fql
├── navigate_back.fql
├── non-200.fql
├── navigate_forward.fql
├── inner_text_all.fql
├── iframes.fql
├── disable-images.fql
├── wait_response.fql
├── navigate_back_2.fql
├── navigate.fql
├── navigate_forward_2.fql
├── wait_request.fql
├── dynamic-page.fql
├── static-page.fql
├── history-api.fql
├── pagination_while.fql
├── while.fql
└── google-search.fql
├── assets
├── logo.png
└── intro.jpg
├── .github
├── ISSUE_TEMPLATE
│ ├── question.md
│ ├── feature_request.md
│ └── bug_report.md
├── FUNDING.yml
└── dependabot.yml
├── .codecov.yml
├── options.go
└── revive.toml
/pkg/drivers/cdp/eval/helpers_test.go:
--------------------------------------------------------------------------------
1 | package eval
2 |
--------------------------------------------------------------------------------
/e2e/pages/dynamic/components/pages/events/countable.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.md]
2 | trim_trailing_whitespace = false
3 |
--------------------------------------------------------------------------------
/examples/screenshot.fql:
--------------------------------------------------------------------------------
1 | RETURN SCREENSHOT("https://www.montferret.dev/")
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MontFerret/ferret/HEAD/assets/logo.png
--------------------------------------------------------------------------------
/assets/intro.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MontFerret/ferret/HEAD/assets/intro.jpg
--------------------------------------------------------------------------------
/pkg/runtime/core/consts.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | const (
4 | IgnorableVariable = "_"
5 | )
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask your question
4 |
5 | ---
6 |
7 |
8 |
--------------------------------------------------------------------------------
/e2e/pages/dynamic/index.css:
--------------------------------------------------------------------------------
1 | /* Show it's not fixed to the top */
2 | body {
3 | min-height: 75rem;
4 | }
5 |
--------------------------------------------------------------------------------
/pkg/runtime/core/cloneable.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | type Cloneable interface {
4 | Value
5 | Clone() Cloneable
6 | }
7 |
--------------------------------------------------------------------------------
/examples/blank-page.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("about:blank", true)
2 | NAVIGATE(doc, "https://www.google.com/")
3 | RETURN doc.url
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: []
4 | patreon: ziflex
5 | open_collective: ferret
6 |
--------------------------------------------------------------------------------
/e2e/tests/examples/click.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/click.fql
3 | assert:
4 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/value.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/value.fql
3 | assert:
4 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/iframes.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/iframes.fql
3 | assert:
4 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/static/load.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | T::EQ(doc.url, url)
5 |
6 | RETURN NONE
--------------------------------------------------------------------------------
/examples/download.fql:
--------------------------------------------------------------------------------
1 | LET data = DOWNLOAD("https://github.com/MontFerret/ferret/raw/master/assets/logo.png")
2 |
3 | RETURN { type: "png", data }
--------------------------------------------------------------------------------
/examples/query-all.fql:
--------------------------------------------------------------------------------
1 | let doc = document("https://github.com/MontFerret/ferret", { driver: "cdp" })
2 |
3 | return elements(doc, '[role="row"]')
--------------------------------------------------------------------------------
/e2e/tests/examples/navigate.yamlx:
--------------------------------------------------------------------------------
1 | query:
2 | ref: file://../../../examples/navigate.fql
3 | assert:
4 | text: RETURN T::TRUE(@lab.data.query.result)
--------------------------------------------------------------------------------
/examples/xpath.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://www.google.ca", {
2 | driver: 'cdp'
3 | })
4 |
5 | RETURN XPATH(doc, "//meta/@charset") // ["UTF-8"]
--------------------------------------------------------------------------------
/e2e/tests/examples/dynamic-page.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/dynamic-page.fql
3 | assert:
4 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/static-page.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/static-page.fql
3 | assert:
4 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/inner_text_all.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/inner_text_all.fql
3 | assert:
4 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/lazy-loading.yaml:
--------------------------------------------------------------------------------
1 |
2 | query:
3 | ref: ../../../examples/lazy-loading.fql
4 | assert:
5 | text: RETURN T::LEN(@lab.data.query.result, 50)
--------------------------------------------------------------------------------
/e2e/tests/examples/navigate_back.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: file://../../../examples/navigate_back.fql
3 | assert:
4 | text: RETURN T::TRUE(@lab.data.query.result)
--------------------------------------------------------------------------------
/pkg/stdlib/testing/base/errors.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import "github.com/pkg/errors"
4 |
5 | var (
6 | ErrAssertion = errors.New("assertion error")
7 | )
8 |
--------------------------------------------------------------------------------
/e2e/tests/examples/navigate_back_2.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: file://../../../examples/navigate_back_2.fql
3 | assert:
4 | text: RETURN T::TRUE(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/navigate_forward.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: file://../../../examples/navigate_forward.fql
3 | assert:
4 | text: RETURN T::TRUE(@lab.data.query.result)
--------------------------------------------------------------------------------
/pkg/drivers/events.go:
--------------------------------------------------------------------------------
1 | package drivers
2 |
3 | const (
4 | NavigationEvent = "navigation"
5 | RequestEvent = "request"
6 | ResponseEvent = "response"
7 | )
8 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/iframes/length.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe"
2 | LET doc = DOCUMENT(url, { driver: 'cdp' })
3 |
4 | RETURN T::LEN(doc.frames, 2)
--------------------------------------------------------------------------------
/e2e/tests/examples/navigate_forward_2.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: file://../../../examples/navigate_forward_2.fql
3 | assert:
4 | text: RETURN T::TRUE(@lab.data.query.result)
--------------------------------------------------------------------------------
/examples/value.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://www.google.com/search?q=foobar", { driver: "cdp" })
2 | LET input = ELEMENT(doc, 'input[name="q"]')
3 |
4 | RETURN input.value
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/navigate/navigate.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET p = DOCUMENT("", { driver: "cdp" })
3 |
4 | NAVIGATE(p, url)
5 |
6 | RETURN T::EQ(p.url, url + '/#/')
--------------------------------------------------------------------------------
/e2e/tests/examples/wait_request.yaml:
--------------------------------------------------------------------------------
1 | timeout: 240
2 | query:
3 | ref: file://../../../examples/wait_request.fql
4 | assert:
5 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/wait_response.yaml:
--------------------------------------------------------------------------------
1 | timeout: 240
2 | query:
3 | ref: file://../../../examples/wait_response.fql
4 | assert:
5 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/examples/blank-page.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/blank-page.fql
3 | assert:
4 | text: |
5 | RETURN T::EQ(@lab.data.query.result, "https://www.google.com/")
6 |
--------------------------------------------------------------------------------
/e2e/tests/examples/split-new-line.yaml:
--------------------------------------------------------------------------------
1 | timeout: 240
2 | query:
3 | ref: file://../../../examples/split-new-line.fql
4 | assert:
5 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/pkg/drivers/cdp/eval/value.go:
--------------------------------------------------------------------------------
1 | package eval
2 |
3 | import "github.com/mafredri/cdp/protocol/runtime"
4 |
5 | type RemoteValue interface {
6 | RemoteID() runtime.RemoteObjectID
7 | }
8 |
--------------------------------------------------------------------------------
/e2e/tests/static/doc/xpath/count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET page = DOCUMENT(url)
3 |
4 | LET actual = XPATH(page, "count(//body)")
5 |
6 | RETURN T::EQ(actual, 1)
--------------------------------------------------------------------------------
/examples/split-new-line.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT('https://github.com/events', {driver: 'cdp'})
2 | LET list = ELEMENT(doc, '.footer')[0]
3 | LET str = INNER_TEXT(list)
4 | RETURN SPLIT(str, "\\n")
--------------------------------------------------------------------------------
/pkg/runtime/errors.go:
--------------------------------------------------------------------------------
1 | package runtime
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | )
6 |
7 | var (
8 | ErrMissedParam = errors.New("missed value for parameter(s)")
9 | )
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/xpath/count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET page = DOCUMENT(url, { driver: "cdp" })
3 |
4 | LET actual = XPATH(page, "count(//body)")
5 |
6 | RETURN T::EQ(actual, 1)
--------------------------------------------------------------------------------
/pkg/runtime/core/predicate.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "context"
4 |
5 | type Predicate interface {
6 | Expression
7 | Eval(ctx context.Context, left, right Value) (Value, error)
8 | }
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_html/get_by_css_not_found.fail.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | RETURN INNER_HTML(el, "h5")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_text/get_by_css_not_found.fail.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | RETURN INNER_TEXT(el, "h5")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/element_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(doc, "#root")?
5 |
6 | T::NOT::NONE(el)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/e2e/tests/examples/google-search.yaml:
--------------------------------------------------------------------------------
1 | query:
2 | ref: ../../../examples/google-search.fql
3 | params:
4 | criteria: "ferret"
5 | assert:
6 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/examples/headers.fql:
--------------------------------------------------------------------------------
1 | LET proxy_header = {"Proxy-Authorization": ["Basic e40b7d5eff464a4fb51efed2d1a19a24"]}
2 |
3 | LET doc = DOCUMENT("https://google.com", { headers: proxy_header})
4 |
5 | RETURN doc
--------------------------------------------------------------------------------
/examples/redirects.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@url, { driver: 'cdp' })
2 |
3 | CLICK(doc, '.click')
4 |
5 | WAITFOR EVENT "navigation" IN doc { target: @targetURL }
6 |
7 | RETURN ELEMENT(doc, '.title')
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/xpath/attr.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET page = DOCUMENT(url, { driver: "cdp" })
3 |
4 | LET actual = XPATH(page, "//meta/@charset")
5 |
6 | RETURN T::EQ(actual, ["utf-8"])
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_text/set_by_css.fail.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | RETURN INNER_TEXT_SET(el, "h4", "foobar")
--------------------------------------------------------------------------------
/e2e/tests/static/doc/xpath/query.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/value.html'
2 | LET page = DOCUMENT(url)
3 |
4 | LET actual = XPATH(page, "//tr[contains(@class, 'odd')]")
5 |
6 | RETURN T::LEN(actual, 20)
--------------------------------------------------------------------------------
/pkg/drivers/errors.go:
--------------------------------------------------------------------------------
1 | package drivers
2 |
3 | import "github.com/pkg/errors"
4 |
5 | var (
6 | ErrDetached = errors.New("element detached")
7 | ErrNotFound = errors.New("element(s) not found")
8 | )
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_html/get_by_xpath_not_found.fail.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | RETURN INNER_HTML(el, X(".//h5"))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_text/get_by_xpath_not_found.fail.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | RETURN INNER_TEXT(el, X(".//h5"))
--------------------------------------------------------------------------------
/e2e/tests/static/doc/xpath/attr.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/simple.html'
2 | LET page = DOCUMENT(url)
3 |
4 | LET actual = XPATH(page, "string(//meta/@charset)")
5 |
6 | RETURN T::EQ(actual, "UTF-8")
7 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_text/set_by_xpath.fail.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | RETURN INNER_TEXT_SET(el, X(".//h4"), "foobar")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/element_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(doc, X("//*[@id='root']"))?
5 |
6 | T::NOT::NONE(el)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/xpath/count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(page, 'main')
5 | LET actual = XPATH(el, "count(//p)")
6 |
7 | RETURN T::EQ(actual, 1)
--------------------------------------------------------------------------------
/e2e/pages/static/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 | Hello world
9 |
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/load.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 |
4 | LET expected = url + '/#/'
5 | LET actual = doc.url
6 |
7 | T::EQ(actual, expected)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/children/count_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET len = ELEMENTS_COUNT(doc, ".track-details")
5 |
6 | T::EQ(len, 20)
7 |
8 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/element_not_found_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 |
4 | LET el = ELEMENT(doc, "#do-not-exist")
5 |
6 | T::NONE(el)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/children/count.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET list = ELEMENT(doc, ".track-list")
4 |
5 | T::EQ(list.length, 20)
6 | T::LEN(list, 20)
7 |
8 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/element_not_found_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(doc, X("//*[@id='do-not-exist']"))?
5 |
6 | T::NONE(el)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/xpath/attrs.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET actual = XPATH(page, "//body/@class")
5 |
6 | T::NOT::EMPTY(actual)
7 |
8 | RETURN T::EQ(actual[0], "text-center")
--------------------------------------------------------------------------------
/examples/click.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://www.montferret.dev/", { driver: "cdp" })
2 |
3 | CLICK(doc, "#repl")
4 |
5 | WAITFOR EVENT "navigation" IN doc
6 | WAIT_ELEMENT(doc, '.code-editor-text')
7 |
8 | RETURN doc.url
9 |
--------------------------------------------------------------------------------
/examples/navigate_back.fql:
--------------------------------------------------------------------------------
1 | LET origin = "https://github.com/"
2 | LET doc = DOCUMENT(origin, { driver: "cdp" })
3 |
4 | NAVIGATE(doc, "https://github.com/features", 10000)
5 | NAVIGATE_BACK(doc)
6 |
7 | RETURN doc.url == origin
8 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/elements_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET els = ELEMENTS(doc, ".form-control")
5 |
6 | T::NOT::EMPTY(els)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/e2e/tests/examples/pagination.yaml:
--------------------------------------------------------------------------------
1 | timeout: 240
2 | query:
3 | ref: file://../../../examples/pagination.fql
4 | params:
5 | criteria: "scraper"
6 | pages: 2
7 | assert:
8 | text: RETURN T::NOT::NONE(@lab.data.query.result)
--------------------------------------------------------------------------------
/pkg/drivers/cdp/events/helpers.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "hash/fnv"
5 | )
6 |
7 | func New(name string) ID {
8 | h := fnv.New32a()
9 |
10 | h.Write([]byte(name))
11 |
12 | return ID(h.Sum32())
13 | }
14 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/element/exists_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 |
4 | T::TRUE(ELEMENT_EXISTS(doc, '.text-center'))
5 | T::FALSE(ELEMENT_EXISTS(doc, '.foo-bar'))
6 |
7 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/xpath/query.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET page = DOCUMENT(url, { driver: "cdp" })
3 |
4 | LET actual = XPATH(page, "//div[contains(@class, 'form-group')]")
5 |
6 | RETURN T::LEN(actual, 4)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/focus/focus_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | FOCUS(page, "#focus-input")
5 |
6 | WAIT_CLASS(page, "#focus-content", "alert-success")
7 |
8 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/static/doc/element/exists_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | T::TRUE(ELEMENT_EXISTS(doc, '.section-nav'))
5 | T::FALSE(ELEMENT_EXISTS(doc, '.foo-bar'))
6 |
7 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/drivers/consts.go:
--------------------------------------------------------------------------------
1 | package drivers
2 |
3 | const (
4 | DefaultPageLoadTimeout = 60000
5 | DefaultWaitTimeout = 5000
6 | DefaultKeyboardDelay = 80
7 | DefaultMouseDelay = 40
8 | DefaultTimeout = 30000
9 | )
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/focus/focus_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, { driver: "cdp" })
3 |
4 | FOCUS(page, "#focus-input")
5 |
6 | WAIT_CLASS(page, "#focus-content", "alert-success")
7 |
8 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/static/element/children/count_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET len = ELEMENTS_COUNT(doc, X("//*[contains(@class, 'track-details')]"))
5 |
6 | T::EQ(len, 20)
7 |
8 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/xpath/count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/value.html'
2 | LET page = DOCUMENT(url)
3 |
4 | LET el = ELEMENT(page, '#listings_table')
5 | LET actual = XPATH(el, "count(//tr)")
6 |
7 | T::EQ(actual, 41)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/children/get_by_index.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET list = ELEMENT(doc, ".track-list")
4 | T::NOT::NONE(list.children[0])
5 | T::NOT::NONE(list.children[1])
6 |
7 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/focus/focus_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | FOCUS(page, X('//*[@id="focus-input"]'))
5 |
6 | WAIT_CLASS(page, "#focus-content", "alert-success")
7 |
8 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/examples/pagination_uncontrolled.yaml:
--------------------------------------------------------------------------------
1 | timeout: 240
2 | query:
3 | ref: file://../../../examples/pagination_uncontrolled.fql
4 | params:
5 | criteria: "scraper"
6 | pages: 2
7 | assert:
8 | text: RETURN T::NOT::EMPTY(@lab.data.query.result)
--------------------------------------------------------------------------------
/e2e/tests/static/element/children/get_by_index.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET list = ELEMENT(doc, ".track-list")
5 |
6 | T::NOT::NONE(list.children[0])
7 | T::NOT::NONE(list.children[1])
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/examples/non-200.fql:
--------------------------------------------------------------------------------
1 | LET p = DOCUMENT('https://www.g2.com/categories', {
2 | ignore: {
3 | statusCodes: [
4 | {
5 | code: 403
6 | }
7 | ]
8 | }
9 | })
10 |
11 | RETURN p.response.statusCode
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/click/click.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, { driver: "cdp" })
3 |
4 | CLICK(page, "#wait-class-random-btn")
5 |
6 | WAIT_CLASS(page, "#wait-class-random-content", "alert-success")
7 |
8 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/focus/focus_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, { driver: "cdp" })
3 |
4 | FOCUS(page, X("//*[@id='focus-input']"))
5 |
6 | WAIT_CLASS(page, "#focus-content", "alert-success")
7 |
8 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/children/get.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET list = ELEMENT(doc, ".track-list")
4 | LET children = list.children
5 | T::NOT::NONE(children)
6 | T::NOT::EMPTY(children)
7 |
8 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/elements_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET els = ELEMENTS(doc, X(".//*[contains(@class, 'form-control')]"))
5 |
6 | T::NOT::EMPTY(els)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/templates/url.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
4 |
5 | const getURL = `() => window.location.toString()`
6 |
7 | func GetURL() *eval.Function {
8 | return eval.F(getURL)
9 | }
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/siblings/next_not_found.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET current = ELEMENT(doc, "body")
4 | T::NOT::NONE(current)
5 | LET next = current.nextElementSibling
6 | T::NONE(next)
7 |
8 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/children/get_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET list = ELEMENT(doc, ".track-list")
5 | LET children = list.children
6 | T::NOT::NONE(children)
7 | T::NOT::EMPTY(children)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/exists.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(doc, "#root")
5 |
6 | T::TRUE(ELEMENT_EXISTS(el, '.jumbotron'))
7 | T::FALSE(ELEMENT_EXISTS(el, '.foo-bar'))
8 |
9 | RETURN NONE
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/siblings/prev_not_found.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET current = ELEMENT(doc, 'head')
4 | T::NOT::NONE(current)
5 | LET prev = current.previousElementSibling
6 | T::NONE(prev)
7 |
8 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/query/exists_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/value.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET el = ELEMENT(doc, "#listings_table")
5 |
6 | T::TRUE(ELEMENT_EXISTS(el, '.odd'))
7 | T::FALSE(ELEMENT_EXISTS(el, '.foo-bar'))
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/compiler/options.go:
--------------------------------------------------------------------------------
1 | package compiler
2 |
3 | type (
4 | Option func(opts *Options)
5 | Options struct {
6 | noStdlib bool
7 | }
8 | )
9 |
10 | func WithoutStdlib() Option {
11 | return func(opts *Options) {
12 | opts.noStdlib = true
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/input/helpers.go:
--------------------------------------------------------------------------------
1 | package input
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | )
8 |
9 | func randomDuration(delay int) time.Duration {
10 | return time.Duration(core.Random2(float64(delay)))
11 | }
12 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/click/click_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | T::TRUE(CLICK(page, "#wait-class-random button"))
5 |
6 | WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000)
7 |
8 | RETURN ""
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/pagination_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/pagination"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET items = (
5 | FOR i IN PAGINATION(page, 'li[class="page-item-next page-item"]')
6 | RETURN i
7 | )
8 |
9 | RETURN T::LEN(items, 5)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/focus/focus.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET input = ELEMENT(page, "#focus-input")
5 |
6 | FOCUS(input)
7 |
8 | WAIT_CLASS(page, "#focus-content", "alert-success")
9 |
10 | RETURN ""
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/xpath/query.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET element = ELEMENT(page, '#page-form')
5 | LET actual = XPATH(element, "//div[contains(@class, 'form-group')]")
6 |
7 | RETURN T::LEN(actual, 4)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/parent/get.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET child = ELEMENT(doc, ".track-list")
4 | LET parent = child.parentElement
5 |
6 | T::NOT::NONE(parent)
7 | T::EQ(parent.attributes.id, "tracks")
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/press/press_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | PRESS_SELECTOR(page, "#press-input", "Enter")
5 |
6 | WAIT(100)
7 |
8 | T::EQ(INNER_TEXT(page, "#press-content"), "Enter")
9 |
10 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/value/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(doc, "#select_input")
5 |
6 | LET expected = "1"
7 | LET actual = el.value
8 |
9 | T::EQ(actual, expected)
10 |
11 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/xpath/query.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/value.html'
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET element = ELEMENT(page, '.tablesorter')
5 | LET actual = XPATH(element, "//input[contains(@type, 'hidden')]")
6 |
7 | T::LEN(actual, 40)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/doc/element/exists_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | T::TRUE(ELEMENT_EXISTS(doc, X("//[contains(@class, 'section-nav')]")))
5 | T::FALSE(ELEMENT_EXISTS(doc, X("//[contains(@class, 'foo-bar')]")))
6 |
7 | RETURN NONE
--------------------------------------------------------------------------------
/examples/navigate_forward.fql:
--------------------------------------------------------------------------------
1 | LET origin = "https://github.com/"
2 | LET target = "https://github.com/features"
3 | LET doc = DOCUMENT(origin, { driver: "cdp" })
4 |
5 | NAVIGATE(doc, target)
6 | NAVIGATE_BACK(doc)
7 | NAVIGATE_FORWARD(doc)
8 |
9 | RETURN doc.url == target
10 |
--------------------------------------------------------------------------------
/e2e/pages/dynamic/index.js:
--------------------------------------------------------------------------------
1 | import AppComponent from "./components/app.js";
2 | import { parse } from "./utils/qs.js";
3 |
4 | const qs = parse(location.search);
5 |
6 | ReactDOM.render(
7 | React.createElement(AppComponent, qs),
8 | document.getElementById("root")
9 | );
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/click/click.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET btn = ELEMENT(page, "#wait-class-random-btn")
5 |
6 | CLICK(btn)
7 |
8 | WAIT_CLASS(page, "#wait-class-random-content", "alert-success")
9 |
10 | RETURN ""
--------------------------------------------------------------------------------
/e2e/tests/static/element/parent/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET child = ELEMENT(doc, ".track-list")
5 | LET parent = child.parentElement
6 |
7 | T::NOT::NONE(parent)
8 | T::EQ(parent.attributes.id, "tracks")
9 |
10 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/click/click_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | T::TRUE(CLICK(page, X("//button[@id='wait-class-random-btn']")))
5 |
6 | WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000)
7 |
8 | RETURN TRUE
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/element/exists_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 |
4 | T::TRUE(ELEMENT_EXISTS(doc, X(".//*[contains(@class, 'text-center')]")))
5 | T::FALSE(ELEMENT_EXISTS(doc, X(".//*[contains(@class, 'foo-bar')]")))
6 |
7 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/press/press_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | PRESS_SELECTOR(page, X("//*[@id='press-input']"), "Enter")
5 |
6 | WAIT(100)
7 |
8 | T::EQ(INNER_TEXT(page, "#press-content"), "Enter")
9 |
10 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/children/get_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET list = ELEMENT(doc, X("//*[contains(@class, 'track-list')]"))
5 | LET children = list.children
6 | T::NOT::NONE(children)
7 | T::NOT::EMPTY(children)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/pages/dynamic/utils/random.js:
--------------------------------------------------------------------------------
1 | export default function random(min = 1000, max = 4000) {
2 | const val = Math.random() * 1000 * 10;
3 |
4 | if (val < min) {
5 | return min;
6 | }
7 |
8 | if (val > max) {
9 | return max;
10 | }
11 |
12 | return val;
13 | }
--------------------------------------------------------------------------------
/examples/inner_text_all.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT('https://soundcloud.com/charts/top', { driver: "cdp" })
2 |
3 | WAIT_ELEMENT(doc, '.chartTrack__details', 5000)
4 |
5 | LET tracks = ELEMENTS(doc, '.chartTrack')
6 |
7 | FOR track IN tracks
8 | RETURN INNER_TEXT_ALL(track, '.chartTrack__details')
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/input/input_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET output = ELEMENT(doc, "#text_output")
7 |
8 | INPUT(doc, "#text_input", "foo")
9 |
10 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/examples/iframes.fql:
--------------------------------------------------------------------------------
1 | LET page = DOCUMENT("https://www.w3schools.com/html/html_iframe.asp", { driver: "cdp" })
2 |
3 | LET content = (
4 | FOR f IN page.frames
5 | FILTER f.URL == "https://www.w3schools.com/html/default.asp"
6 | RETURN f.head.innerHTML
7 | )
8 |
9 | RETURN FIRST(content)
--------------------------------------------------------------------------------
/examples/disable-images.fql:
--------------------------------------------------------------------------------
1 | LET p = DOCUMENT("https://www.gettyimages.com/", {
2 | driver: "cdp",
3 | ignore: {
4 | resources: [
5 | {
6 | url: "*",
7 | type: "image"
8 | }
9 | ]
10 | }
11 | })
12 |
13 | RETURN TRUE
14 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/iframes/lookup.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe"
2 | LET page = DOCUMENT(url, { driver: 'cdp', timeout: 5000 })
3 |
4 | LET frames = (
5 | FOR frame IN page.frames
6 | FILTER frame.name == "nested"
7 | RETURN frame
8 | )
9 |
10 | RETURN T::LEN(frames, 1)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/input/input_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET output = ELEMENT(doc, "#text_output")
7 |
8 | INPUT(doc, X("//*[@id='text_input']"), "foo")
9 |
10 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/click/click_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET div = ELEMENT(page, "#wait-class-random")
5 |
6 | CLICK(div, "button")
7 |
8 | WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000)
9 |
10 | RETURN TRUE
11 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/waitfor_event/navigation.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/navigation"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 |
4 | INPUT(page, "#url", "https://getbootstrap.com/")
5 | CLICK(page, "#submit")
6 |
7 | WAITFOR EVENT "navigation" IN page
8 |
9 | RETURN T::EQ(page.URL, "https://getbootstrap.com/")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/click/click_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET div = ELEMENT(page, "#wait-class-random")
5 |
6 | CLICK(div, X(".//button"))
7 |
8 | WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000)
9 |
10 | RETURN TRUE
11 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/press/press.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET input = ELEMENT(page, "#press-input")
5 |
6 | FOCUS(input)
7 |
8 | PRESS(page, "Enter")
9 |
10 | WAIT(100)
11 |
12 | T::EQ(INNER_TEXT(page, "#press-content"), "Enter")
13 |
14 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/press/press_multi.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET input = ELEMENT(page, "#press-input")
5 |
6 | FOCUS(input)
7 | INPUT(input, "foo")
8 |
9 | PRESS(page, "Backspace", 3) // Clear
10 |
11 | T::EQ(input.value, "")
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/utils/lib.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | func RegisterLib(ns core.Namespace) error {
6 | return ns.RegisterFunctions(
7 | core.NewFunctionsFromMap(map[string]core.Function{
8 | "WAIT": Wait,
9 | "PRINT": Print,
10 | }),
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_text/get_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = 'ContainersResponsive breakpointsZ-index'
5 | LET actual = INNER_TEXT(doc, '.section-nav')
6 |
7 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/e2e/tests/static/element/attrs/get_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET element = ELEMENT(doc, X("//body/header/a"))
5 | LET actual = XPATH(element, "string(@href)")
6 | LET expected = "http://getbootstrap.com/"
7 |
8 | T::EQ(actual, expected)
9 |
10 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/query/exists_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/value.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET el = ELEMENT(doc, "#listings_table")
5 |
6 | T::TRUE(ELEMENT_EXISTS(el, X("//*[contains(@class, 'odd')]")))
7 | T::FALSE(ELEMENT_EXISTS(el, X("//*[contains(@class, 'foo-bar')]")))
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/input/input.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET input = ELEMENT(doc, "#text_input")
7 | LET output = ELEMENT(doc, "#text_output")
8 |
9 | INPUT(input, "foo")
10 |
11 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/query/exists_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET el = ELEMENT(doc, "#root")
5 |
6 | T::TRUE(ELEMENT_EXISTS(el, X(".//*[contains(@class, 'jumbotron')]")))
7 | T::FALSE(ELEMENT_EXISTS(el, X(".//*[contains(@class, 'foo-bar')]")))
8 |
9 | RETURN NONE
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/cookies/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(@lab.cdn.dynamic, {
3 | driver: "cdp"
4 | })
5 |
6 | COOKIE_SET(doc, {
7 | name: "x-e2e",
8 | value: "test"
9 | })
10 |
11 | LET cookie = COOKIE_GET(doc, "x-e2e")
12 | LET expected = "test"
13 |
14 | RETURN T::EQ(cookie.value, expected)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/blur/blur_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | FOCUS(page, "#focus-input")
5 |
6 | WAIT_CLASS(page, "#focus-content", "alert-success")
7 |
8 | BLUR(page, "#focus-input")
9 |
10 | WAIT_NO_CLASS(page, "#focus-content", "alert-success")
11 |
12 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/static/doc/cookies/get.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET doc = DOCUMENT(url, {
3 | driver: "http"
4 | })
5 |
6 | LET cookiesPath = LENGTH(doc.cookies) > 0 ? "ok" : "false"
7 | LET cookie = COOKIE_GET(doc, "x-ferret")
8 | LET expected = "ok e2e"
9 |
10 | RETURN T::EQ(cookiesPath + " " + cookie.value, expected)
--------------------------------------------------------------------------------
/examples/wait_response.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT('https://soundcloud.com/charts/top', { driver: "cdp" })
2 |
3 | WAIT_ELEMENT(doc, '.chartTrack__details', 5000)
4 | SCROLL_BOTTOM(doc)
5 |
6 | LET evt = (WAITFOR EVENT "response" IN doc FILTER CURRENT.url LIKE "https://api-v2.soundcloud.com/charts?genre=soundcloud*")
7 |
8 | RETURN JSON_PARSE(evt.body)
9 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/pagination_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/pagination"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET items = (
5 | FOR i IN PAGINATION(page, X("//li[contains(@class, 'page-item-next') and contains(@class, 'page-item') and not(contains(@class, 'disabled'))]"))
6 | RETURN i
7 | )
8 |
9 | RETURN T::LEN(items, 5)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/class_all_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
7 | WAIT_CLASS_ALL(doc, "#wait-class-content, #wait-class-random-content", "alert-success", 10000)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/cookies/load_with.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, {
3 | driver: "cdp",
4 | cookies: [{
5 | name: "x-e2e",
6 | value: "test"
7 | }]
8 | })
9 |
10 | LET cookie = COOKIE_GET(doc, "x-e2e")
11 |
12 | T::NOT::NONE(cookie)
13 | T::EQ(cookie.value, "test")
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_text/get_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = 'ContainersResponsive breakpointsZ-index'
5 | LET actual = INNER_TEXT(doc, X("//*[contains(@class, 'section-nav')]"))
6 |
7 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/blur/blur.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET input = ELEMENT(page, "#focus-input")
5 |
6 | FOCUS(input)
7 |
8 | WAIT_CLASS(page, "#focus-content", "alert-success")
9 |
10 | BLUR(input)
11 |
12 | WAIT_NO_CLASS(page, "#focus-content", "alert-success")
13 |
14 | RETURN ""
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/blur/blur_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/events"
2 | LET page = DOCUMENT(url, true)
3 |
4 | FOCUS(page, "#focus-input")
5 |
6 | WAIT_CLASS(page, "#focus-content", "alert-success")
7 |
8 | BLUR(page, X('.//*[@id="focus-input"]'))
9 |
10 | WAIT_NO_CLASS(page, "#focus-content", "alert-success")
11 |
12 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/input/input_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET form = ELEMENT(doc, "#page-form")
7 |
8 | INPUT(form, "#text_input", "foo")
9 |
10 | LET output = ELEMENT(doc, "#text_output")
11 |
12 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/static/parse.fql:
--------------------------------------------------------------------------------
1 | LET page = PARSE(`
2 |
3 |
4 |
5 |
6 | Offline
7 |
8 |
9 | Hello world
10 |
11 |
12 | `)
13 |
14 | LET title = ELEMENT(page, "title")
15 |
16 | T::EQ(title.innerText, "Offline")
17 |
18 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/attr_all_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
7 | WAIT_ATTR_ALL(doc, "#wait-class-content, #wait-class-random-content", "class", "alert alert-success", 10000)
8 |
9 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/click/click_with_count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(page, "form")
5 |
6 | LET input = ELEMENT(page, "#text_input")
7 |
8 | INPUT(input, "Foo")
9 |
10 | CLICK(input, 2)
11 |
12 | INPUT(input, "Bar")
13 |
14 | WAIT(100)
15 |
16 | RETURN T::EQ(input.value, "Bar")
--------------------------------------------------------------------------------
/examples/navigate_back_2.fql:
--------------------------------------------------------------------------------
1 | LET origin = "https://github.com/"
2 | LET doc = DOCUMENT(origin, { driver: "cdp" })
3 |
4 | NAVIGATE(doc, "https://github.com/features", 10000)
5 | NAVIGATE(doc, "https://github.com/business", 10000)
6 | NAVIGATE(doc, "https://github.com/marketplace", 10000)
7 | NAVIGATE_BACK(doc, 3, 10000)
8 |
9 | RETURN doc.url == origin
10 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_class_all_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | CLICK_ALL(doc, "#wait-no-class-btn, #wait-no-class-random-btn")
7 | WAIT_NO_CLASS_ALL(doc, "#wait-no-class-content, #wait-no-class-random-content", "alert-success", 10000)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/cookies/get.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, {
3 | driver: "cdp"
4 | })
5 |
6 | LET cookiesPath = LENGTH(doc.cookies) > 0 ? "ok" : "false"
7 | LET cookie = COOKIE_GET(doc, "x-ferret")
8 | LET expected = "ok e2e"
9 |
10 | T::LEN(doc.cookies, 1)
11 |
12 | RETURN T::EQ(cookiesPath + " " + cookie.value, expected)
--------------------------------------------------------------------------------
/examples/navigate.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://github.com/", { driver: "cdp" })
2 | LET main = ELEMENT(doc, '.application-main')
3 | LET mainTxt = main.innerText
4 |
5 | NAVIGATE(doc, "https://github.com/features", 10000)
6 |
7 | LET features = ELEMENT(doc, '.application-main')
8 | LET featuresTxt = features.innerText
9 |
10 | RETURN mainTxt != featuresTxt
11 |
--------------------------------------------------------------------------------
/examples/navigate_forward_2.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://github.com/", { driver: "cdp" })
2 |
3 | NAVIGATE(doc, "https://github.com/features")
4 | NAVIGATE(doc, "https://github.com/enterprise")
5 | NAVIGATE(doc, "https://github.com/marketplace")
6 | NAVIGATE_BACK(doc, 3)
7 | NAVIGATE_FORWARD(doc, 2)
8 |
9 | RETURN doc.url == "https://github.com/enterprise"
10 |
--------------------------------------------------------------------------------
/examples/wait_request.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT('https://soundcloud.com/charts/top', { driver: "cdp", userAgent: "*" })
2 |
3 | WAIT_ELEMENT(doc, '.chartTrack__details', 5000)
4 | SCROLL_BOTTOM(doc)
5 |
6 | LET evt = (WAITFOR EVENT "request" IN doc FILTER CURRENT.url LIKE "https://api-v2.soundcloud.com/charts?genre=soundcloud*")
7 |
8 | RETURN evt.headers["User-Agent"]
9 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/templates/parent.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "github.com/mafredri/cdp/protocol/runtime"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
7 | )
8 |
9 | const getParent = "(el) => el.parentElement"
10 |
11 | func GetParent(id runtime.RemoteObjectID) *eval.Function {
12 | return eval.F(getParent).WithArgRef(id)
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/drivers/common/path.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | )
8 |
9 | func PathToString(path []core.Value) string {
10 | spath := make([]string, 0, len(path))
11 |
12 | for i, s := range path {
13 | spath[i] = s.String()
14 | }
15 |
16 | return strings.Join(spath, ".")
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/stdlib/collections/lib.go:
--------------------------------------------------------------------------------
1 | package collections
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | func RegisterLib(ns core.Namespace) error {
6 | return ns.RegisterFunctions(
7 | core.NewFunctionsFromMap(map[string]core.Function{
8 | "INCLUDES": Includes,
9 | "LENGTH": Length,
10 | "REVERSE": Reverse,
11 | }))
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/click/click_with_count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(page, "form")
5 |
6 | LET input = ELEMENT(page, "#text_input")
7 |
8 | INPUT(input, "Foo")
9 |
10 | CLICK(page, "#text_input", 2)
11 |
12 | INPUT(input, "Bar")
13 |
14 | WAIT(100)
15 |
16 | RETURN T::EQ(input.value, "Bar")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/select/single_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET output = ELEMENT(doc, "#select_output")
7 | LET result = SELECT(doc, "#select_input", ["4"])
8 |
9 | T::EQ(output.innerText, "4")
10 | T::EQ(JSON_STRINGIFY(result), '["4"]')
11 |
12 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/class_all_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
7 | WAIT_CLASS_ALL(doc, X("//*[@id='wait-class-content' or @id='wait-class-random-content']"), "alert-success", 10000)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/input/input_by_css_with_timeout.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET form = ELEMENT(doc, "#page-form")
7 |
8 | INPUT(form, "#text_input", "foo", 100)
9 |
10 | LET output = ELEMENT(doc, "#text_output")
11 |
12 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/input/input_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET form = ELEMENT(doc, "#page-form")
7 |
8 | INPUT(form, X('//*[@id="text_input"]'), "foo")
9 |
10 | LET output = ELEMENT(doc, "#text_output")
11 |
12 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_text/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET expected = `Hello`
5 |
6 | INNER_TEXT_SET(doc, "body", expected)
7 |
8 | LET actual = INNER_TEXT(doc, "body")
9 |
10 | LET r1 = '(\s|\")'
11 | LET r2 = '(\n|\s|\")'
12 |
13 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/hover/hover_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | HOVER(doc, "#hoverable-btn")
7 | WAIT_ELEMENT(doc, "#hoverable-content")
8 |
9 | LET output = INNER_TEXT(doc, "#hoverable-content")
10 |
11 | RETURN T::EQ(output, "Lorem ipsum dolor sit amet.")
--------------------------------------------------------------------------------
/pkg/stdlib/io/fs/util_test.go:
--------------------------------------------------------------------------------
1 | package fs_test
2 |
3 | import (
4 | "os"
5 |
6 | . "github.com/smartystreets/goconvey/convey"
7 | )
8 |
9 | func tempFile() (*os.File, func()) {
10 | file, err := os.CreateTemp("", "fstest")
11 | So(err, ShouldBeNil)
12 |
13 | fn := func() {
14 | file.Close()
15 | os.Remove(file.Name())
16 | }
17 |
18 | return file, fn
19 | }
20 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/attrs/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 |
4 | LET el = ELEMENT(doc, "#index")
5 | LET attrs = [
6 | el.attributes.class,
7 | el.attributes["data-type"]
8 | ]
9 |
10 | LET expected = '["jumbotron","page"]'
11 | LET actual = TO_STRING(attrs)
12 |
13 | T::EQ(actual, expected)
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/click/click_by_css_with_count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(page, "form")
5 |
6 | LET input = ELEMENT(page, "#text_input")
7 |
8 | INPUT(input, "Foo")
9 |
10 | CLICK(page, "#text_input", 2)
11 |
12 | INPUT(input, "Bar")
13 |
14 | WAIT(100)
15 |
16 | RETURN T::EQ(input.value, "Bar")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/select/single.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET input = ELEMENT(doc, "#select_input")
7 | LET output = ELEMENT(doc, "#select_output")
8 | LET result = SELECT(input, ["4"])
9 |
10 | T::EQ(output.innerText, "4")
11 | T::EQ(result, ["4"])
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/parse.fql:
--------------------------------------------------------------------------------
1 | LET page = PARSE(`
2 |
3 |
4 |
5 |
6 | Offline
7 |
8 |
9 | Hello world
10 |
11 |
12 | `, {
13 | driver: "cdp"
14 | })
15 |
16 | LET title = ELEMENT(page, "title")
17 |
18 | RETURN T::EQ(title.innerText, "Offline")
--------------------------------------------------------------------------------
/e2e/tests/static/element/inner_text/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/simple.html'
2 | LET doc = DOCUMENT(url)
3 | LET el = ELEMENT(doc, "body")
4 |
5 | LET expected = `Hello world`
6 | LET actual = INNER_TEXT(el)
7 |
8 | LET r1 = '(\s|\")'
9 | LET r2 = '(\n|\s|\")'
10 |
11 | T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/drivers/common/ua.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/corpix/uarand"
5 | )
6 |
7 | const RandomUserAgent = "*"
8 |
9 | func GetUserAgent(val string) string {
10 | if val == "" {
11 | return val
12 | }
13 |
14 | if val != RandomUserAgent {
15 | return val
16 | }
17 |
18 | // TODO: Change the implementation
19 | return uarand.GetRandom()
20 | }
21 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/click/click_by_css_with_count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(page, "form")
5 |
6 | LET input = ELEMENT(page, "#text_input")
7 |
8 | INPUT(input, "Foo")
9 |
10 | T::TRUE(CLICK(page, "#text_input", 2))
11 |
12 | INPUT(input, "Bar")
13 |
14 | WAIT(100)
15 |
16 | RETURN T::EQ(input.value, "Bar")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/iframes/input.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/forms"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET doc = page.frames[1]
4 |
5 | WAIT_ELEMENT(doc, "form")
6 |
7 | LET input = ELEMENT(doc, "#text_input")
8 | LET output = ELEMENT(doc, "#text_output")
9 |
10 | INPUT(input, "foo")
11 |
12 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/input/input_by_xpath_with_timeout.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET form = ELEMENT(doc, "#page-form")
7 |
8 | INPUT(form, X('//*[@id="text_input"]'), "foo", 100)
9 |
10 | LET output = ELEMENT(doc, "#text_output")
11 |
12 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/url/get.fql:
--------------------------------------------------------------------------------
1 | LET page = DOCUMENT(@lab.cdn.dynamic, {
2 | driver: 'cdp'
3 | })
4 |
5 | LET initialDoc = page.frames[0].url
6 | LET initial = page.url
7 |
8 | CLICK(page, ".nav-link-forms")
9 |
10 | LET currentDoc = page.frames[0].url
11 | LET current = page.url
12 |
13 | T::NOT::EQ(initial, current)
14 | T::EQ(initialDoc, currentDoc)
15 |
16 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/drivers/cdp/eval/return.go:
--------------------------------------------------------------------------------
1 | package eval
2 |
3 | type ReturnType int
4 |
5 | const (
6 | ReturnNothing ReturnType = iota
7 | ReturnValue
8 | ReturnRef
9 | )
10 |
11 | func (rt ReturnType) String() string {
12 | switch rt {
13 | case ReturnValue:
14 | return "value"
15 | case ReturnRef:
16 | return "reference"
17 | default:
18 | return "nothing"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_class_all_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | CLICK_ALL(doc, "#wait-no-class-btn, #wait-no-class-random-btn")
7 | WAIT_NO_CLASS_ALL(doc, X("//*[@id='wait-no-class-content' or @id='wait-no-class-random-content']"), "alert-success", 10000)
8 |
9 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/hover/hover_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | HOVER(doc, X('.//*[@id="hoverable-btn"]'))
7 | WAIT_ELEMENT(doc, "#hoverable-content")
8 |
9 | LET output = INNER_TEXT(doc, "#hoverable-content")
10 |
11 | RETURN T::EQ(output, "Lorem ipsum dolor sit amet.")
--------------------------------------------------------------------------------
/e2e/tests/static/element/inner_html/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/simple.html'
2 | LET doc = DOCUMENT(url)
3 | LET el = ELEMENT(doc, "body")
4 |
5 | LET expected = `Hello world
`
6 | LET actual = INNER_HTML(el)
7 |
8 | LET r1 = '(\s|\")'
9 | LET r2 = '(\n|\s|\")'
10 |
11 | T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_text/get_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = '#root > div > main > div h1'
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = 'Welcome to Ferret E2E test page!'
8 | LET actual = INNER_TEXT(doc, selector)
9 |
10 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/styles/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 |
6 | WAIT_ELEMENT(doc, pageSelector)
7 |
8 | LET el = ELEMENT(doc, elemSelector)
9 | LET val = STYLE_GET(el, "display")
10 |
11 | T::EQ(val, {display: "block"})
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_html/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET expected = `Hello`
5 |
6 | INNER_HTML_SET(doc, "body", "Hello")
7 |
8 | LET actual = INNER_HTML(doc, "body")
9 |
10 | LET r1 = '(\s|\")'
11 | LET r2 = '(\n|\s|\")'
12 |
13 | RETURN T::EQ(REGEX_REPLACE(expected, r1, ''), REGEX_REPLACE(TRIM(actual), r2, ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/select/single_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET output = ELEMENT(doc, "#select_output")
7 | LET result = SELECT(doc, X("//*[@id='select_input']"), ["4"])
8 |
9 | T::EQ(output.innerText, "4")
10 | T::EQ(JSON_STRINGIFY(result), '["4"]')
11 |
12 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/attrs/get_2.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 |
6 | WAIT_ELEMENT(doc, pageSelector)
7 |
8 | LET el = ELEMENT(doc, elemSelector)
9 | LET attrs = ATTR_GET(el, "style")
10 |
11 | T::EQ(attrs.style.display, "block")
12 |
13 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/click/click_by_xpath_with_count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(page, "form")
5 |
6 | LET input = ELEMENT(page, "#text_input")
7 |
8 | INPUT(input, "Foo")
9 |
10 | CLICK(page, X('.//*[@id="text_input"]'), 2)
11 |
12 | INPUT(input, "Bar")
13 |
14 | WAIT(100)
15 |
16 | RETURN T::EQ(input.value, "Bar")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_text/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | LET expected = `Foobar`
6 | INNER_TEXT_SET(el, expected)
7 | WAIT(100)
8 | LET actual = INNER_TEXT(el)
9 |
10 | LET r1 = '(\n|\s)'
11 | LET r2 = '(\n|\s)'
12 |
13 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_html/get_by_css_all.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = [
5 | 'Containers',
6 | 'Responsive breakpoints',
7 | 'Z-index'
8 | ]
9 | LET actual = INNER_HTML_ALL(doc, '.section-nav li')
10 |
11 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/pkg/runtime/collections/iterator.go:
--------------------------------------------------------------------------------
1 | package collections
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | )
8 |
9 | type (
10 | Iterator interface {
11 | Next(ctx context.Context, scope *core.Scope) (*core.Scope, error)
12 | }
13 |
14 | Iterable interface {
15 | Iterate(ctx context.Context, scope *core.Scope) (Iterator, error)
16 | }
17 | )
18 |
--------------------------------------------------------------------------------
/pkg/runtime/core/setter.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "context"
4 |
5 | type (
6 | // Setter represents an interface of
7 | // complex types that needs to be used to write values by path.
8 | // The interface is created to let user-defined types be used in dot notation assignment.
9 | Setter interface {
10 | SetIn(ctx context.Context, path []Value, value Value) PathError
11 | }
12 | )
13 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/click/click_by_xpath_with_count.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "/#/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(page, "form")
5 |
6 | LET input = ELEMENT(page, "#text_input")
7 |
8 | INPUT(input, "Foo")
9 |
10 | T::TRUE(CLICK(page, X(".//*[@id='text_input']"), 2))
11 |
12 | INPUT(input, "Bar")
13 |
14 | WAIT(100)
15 |
16 | RETURN T::EQ(input.value, "Bar")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/select/multi_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET output = ELEMENT(doc, "#multi_select_output")
7 | LET result = SELECT(doc, "#multi_select_input", ["1", "2", "4"])
8 |
9 | T::EQ(output.innerText, "1, 2, 4")
10 | T::EQ(JSON_STRINGIFY(result), '["1","2","4"]')
11 |
12 | RETURN NONE
--------------------------------------------------------------------------------
/examples/dynamic-page.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT('https://soundcloud.com/charts/top', { driver: "cdp" })
2 |
3 | WAIT_ELEMENT(doc, '.chartTrack__details', 5000)
4 |
5 | LET tracks = ELEMENTS(doc, '.chartTrack__details')
6 |
7 | FOR track IN tracks
8 | RETURN {
9 | artist: TRIM(INNER_TEXT(track, '.chartTrack__username')),
10 | track: TRIM(INNER_TEXT(track, '.chartTrack__title'))
11 | }
12 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | # Ignore coverage for generated code.
3 | - "pkg/parser"
4 |
5 | coverage:
6 | range: 70..100
7 | round: nearest
8 | precision: 1
9 |
10 | status:
11 | project:
12 | default:
13 | enabled: yes
14 | threshold: 2%
15 | patch: no
16 | changes: no
17 |
18 | comment:
19 | layout: "header, diff"
20 | behavior: once
21 | require_changes: yes
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_text/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | 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."
5 | LET actual = INNER_TEXT(doc, 'body > div > div > main > p.bd-lead')
6 |
7 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/hover/hover.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | LET input = ELEMENT(doc, "#hoverable-btn")
7 |
8 | HOVER(input)
9 | WAIT_ELEMENT(doc, "#hoverable-content")
10 |
11 | LET output = ELEMENT(doc, "#hoverable-content")
12 |
13 | RETURN T::EQ(output.innerText, "Lorem ipsum dolor sit amet.")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/select/multi.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET input = ELEMENT(doc, "#multi_select_input")
7 | LET output = ELEMENT(doc, "#multi_select_output")
8 | LET result = SELECT(input, ["1", "2", "4"])
9 |
10 | T::EQ(output.innerText, "1, 2, 4")
11 | T::EQ(result, ["1","2","4"])
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/attrs/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET el = ELEMENT(doc, "body > header > a")
5 | LET attrs = [
6 | el.attributes.class,
7 | el.attributes.href
8 | ]
9 |
10 | LET expected = '["navbar-brand mr-0 mr-md-2","http://getbootstrap.com/"]'
11 | LET actual = TO_STRING(attrs)
12 |
13 | T::EQ(actual, expected)
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/testing/base/format.go:
--------------------------------------------------------------------------------
1 | package base
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | func FormatValue(val core.Value) string {
11 | valStr := val.String()
12 |
13 | if val == values.None {
14 | valStr = "none"
15 | }
16 |
17 | return fmt.Sprintf("[%s] '%s'", val.Type(), valStr)
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_text/get_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 | LET selector = X('.//*[@id="root"]/div/main/div/*/h1')
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = 'Welcome to Ferret E2E test page!'
8 | LET actual = INNER_TEXT(doc, selector)
9 |
10 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/select/multi_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET output = ELEMENT(doc, "#multi_select_output")
7 | LET result = SELECT(doc, X("//*[@id='multi_select_input']"), ["1", "2", "4"])
8 |
9 | T::EQ(output.innerText, "1, 2, 4")
10 | T::EQ(JSON_STRINGIFY(result), '["1","2","4"]')
11 |
12 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/ua.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/api/ts'
2 | LET ua = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) brave/0.7.10 Chrome/47.0.2526.110 Brave/0.36.5 Safari/537.36"
3 | LET page = DOCUMENT(url, {
4 | userAgent: ua
5 | })
6 |
7 | LET h = ELEMENT(page, "#headers")
8 | LET headers = JSON_PARSE(h.innerText)
9 |
10 | RETURN T::EQ(ua, headers["User-Agent"][0])
11 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/siblings/next.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET current = ELEMENT(doc, ".track")
4 | T::NOT::NONE(current)
5 | LET next = current.nextElementSibling
6 | T::NOT::NONE(next)
7 |
8 | LET currentIdx = TO_INT(current.attributes['data-index'])
9 | LET nextIdx = TO_INT(next.attributes['data-index'])
10 | T::GT(nextIdx, currentIdx)
11 |
12 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/inner_text/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/simple.html'
2 | LET doc = DOCUMENT(url)
3 | LET el = ELEMENT(doc, "body")
4 |
5 | LET expected = `Injected by Ferret`
6 |
7 | INNER_TEXT_SET(el, expected)
8 |
9 | LET actual = INNER_TEXT(el)
10 |
11 | LET r1 = '(\s|\")'
12 | LET r2 = '(\n|\s|\")'
13 |
14 | T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
15 |
16 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/siblings/prev.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT(@lab.cdn.dynamic + "/#/lists", { driver:"cdp" })
2 |
3 | LET current = ELEMENT(doc, '[data-index="1"]')
4 | T::NOT::NONE(current)
5 | LET prev = current.previousElementSibling
6 | T::NOT::NONE(prev)
7 |
8 | LET currentIdx = TO_INT(current.attributes['data-index'])
9 | LET prevIdx = TO_INT(prev.attributes['data-index'])
10 | T::LT(prevIdx, currentIdx)
11 |
12 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/io/fs/lib.go:
--------------------------------------------------------------------------------
1 | package fs
2 |
3 | import (
4 | "github.com/MontFerret/ferret/pkg/runtime/core"
5 | )
6 |
7 | // RegisterLib register `FS` namespace functions.
8 | // @namespace FS
9 | func RegisterLib(ns core.Namespace) error {
10 | return ns.
11 | Namespace("FS").
12 | RegisterFunctions(
13 | core.NewFunctionsFromMap(map[string]core.Function{
14 | "READ": Read,
15 | "WRITE": Write,
16 | }))
17 | }
18 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_html/get_by_css_all.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#layout")
5 |
6 | LET expected = [
7 | 'Welcome to Ferret E2E test page!
',
8 | 'It has several pages for testing different possibilities of the library
'
9 | ]
10 | LET actual = INNER_HTML_ALL(doc, '#root > div > main > div > *')
11 |
12 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/e2e/tests/static/element/siblings/prev.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET current = ELEMENT(doc, '[data-index="1"]')
5 | T::NOT::NONE(current)
6 | LET prev = current.previousElementSibling
7 | T::NOT::NONE(prev)
8 |
9 | LET currentIdx = TO_INT(current.attributes['data-index'])
10 | LET prevIdx = TO_INT(prev.attributes['data-index'])
11 | T::LT(prevIdx, currentIdx)
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/iframes/select_single.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/forms"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET doc = page.frames[1]
4 |
5 | WAIT_ELEMENT(doc, "form")
6 |
7 | LET input = ELEMENT(doc, "#select_input")
8 | LET output = ELEMENT(doc, "#select_output")
9 | LET result = SELECT(input, ["4"])
10 |
11 | T::EQ(output.innerText, "4")
12 | T::EQ(result, ["4"])
13 |
14 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/siblings/next.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/list.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET current = ELEMENT(doc, ".track")
5 | T::NOT::NONE(current)
6 | LET next = current.nextElementSibling.nextElementSibling
7 | T::NOT::NONE(next)
8 |
9 | LET currentIdx = TO_INT(current.attributes['data-index'])
10 | LET nextIdx = TO_INT(next.attributes['data-index'])
11 | T::GT(nextIdx, currentIdx)
12 |
13 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/element/inner_html/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/simple.html'
2 | LET doc = DOCUMENT(url)
3 | LET el = ELEMENT(doc, "body")
4 |
5 | LET expected = `Injected by Ferret
`
6 |
7 | INNER_HTML_SET(el, expected)
8 |
9 | LET actual = INNER_HTML(el)
10 |
11 | LET r1 = '(\s|\")'
12 | LET r2 = '(\n|\s|\")'
13 |
14 | T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
15 |
16 | RETURN NONE
--------------------------------------------------------------------------------
/examples/static-page.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://github.com/topics")
2 |
3 | FOR el IN ELEMENTS(doc, ".py-4.border-bottom")
4 | LIMIT 10
5 | LET url = ELEMENT(el, "a")
6 | LET name = ELEMENT(el, ".f3")
7 | LET description = ELEMENT(el, ".f5")
8 |
9 | RETURN {
10 | name: TRIM(name.innerText),
11 | description: TRIM(description.innerText),
12 | url: "https://github.com" + url.attributes.href
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/eval/arguments.go:
--------------------------------------------------------------------------------
1 | package eval
2 |
3 | import (
4 | "github.com/mafredri/cdp/protocol/runtime"
5 | "github.com/rs/zerolog"
6 | )
7 |
8 | type FunctionArguments []runtime.CallArgument
9 |
10 | func (args FunctionArguments) MarshalZerologArray(a *zerolog.Array) {
11 | for _, arg := range args {
12 | if arg.ObjectID != nil {
13 | a.Str(string(*arg.ObjectID))
14 | } else {
15 | a.RawJSON(arg.Value)
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/compiler/fuzz/fuzz.go:
--------------------------------------------------------------------------------
1 | package compiler_test
2 |
3 | import (
4 | "github.com/MontFerret/ferret/pkg/compiler"
5 | )
6 |
7 | // Fuzz is our fuzzer.
8 | // The fuzzer is run on oss-fuzz.
9 | // Link to the project on oss-fuzz
10 | // will be added once the project is up.
11 | func Fuzz(data []byte) int {
12 | c := compiler.New()
13 | p, err := c.Compile(string(data))
14 | if err != nil || p == nil {
15 | return 0
16 | }
17 | return 1
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/pi_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | m "math"
6 | "testing"
7 |
8 | . "github.com/smartystreets/goconvey/convey"
9 |
10 | "github.com/MontFerret/ferret/pkg/stdlib/math"
11 | )
12 |
13 | func TestPi(t *testing.T) {
14 | Convey("Should return Pi value", t, func() {
15 | out, err := math.Pi(context.Background())
16 |
17 | So(err, ShouldBeNil)
18 | So(out.Unwrap(), ShouldEqual, m.Pi)
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/iframes/element_exists.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 |
4 | LET frames = (
5 | FOR f IN page.frames
6 | FILTER f.name == "nested"
7 | LIMIT 1
8 | RETURN f
9 | )
10 |
11 | LET doc = FIRST(frames)
12 |
13 | T::NOT::NONE(doc)
14 | T::TRUE(ELEMENT_EXISTS(doc, '.text-center'))
15 | T::FALSE(ELEMENT_EXISTS(doc, '.foo-bar'))
16 |
17 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/class_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | CLICK(doc, "#wait-class-btn")
8 | WAIT_CLASS(doc, "#wait-class-content", "alert-success")
9 |
10 | // with random timeout
11 | CLICK(doc, "#wait-class-random-btn")
12 | WAIT_CLASS(doc, "#wait-class-random-content", "alert-success", 10000)
13 |
14 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_text/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, ".jumbotron")
4 |
5 | LET expected = `
6 | Welcome to Ferret E2E test page!
7 | It has several pages for testing different possibilities of the library
8 | `
9 | LET actual = INNER_TEXT(el)
10 |
11 | LET r1 = '(\n|\s)'
12 | LET r2 = '(\n|\s)'
13 |
14 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
--------------------------------------------------------------------------------
/pkg/stdlib/math/rand_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | . "github.com/smartystreets/goconvey/convey"
8 |
9 | "github.com/MontFerret/ferret/pkg/stdlib/math"
10 | )
11 |
12 | func TestRand(t *testing.T) {
13 | Convey("Should return pseudo-random value", t, func() {
14 | out, err := math.Rand(context.Background())
15 |
16 | So(err, ShouldBeNil)
17 | So(out.Unwrap(), ShouldBeLessThan, 1)
18 | })
19 | }
20 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/attr_2.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | LET el = ELEMENT(doc, "#wait-class-content")
7 |
8 | ATTR_SET(el, "data-test", "test")
9 | WAIT_ATTR(el, "data-test", "test")
10 |
11 | ATTR_REMOVE(el, "class")
12 |
13 | WAIT_ATTR(el, "class", NONE)
14 |
15 | T::NONE(el.attributes.class, "attribute should be removed")
16 |
17 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/runtime/expressions/literals/int.go:
--------------------------------------------------------------------------------
1 | package literals
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | type IntLiteral int
11 |
12 | func NewIntLiteral(value int) IntLiteral {
13 | return IntLiteral(value)
14 | }
15 |
16 | func (l IntLiteral) Exec(_ context.Context, _ *core.Scope) (core.Value, error) {
17 | return values.NewInt(int(l)), nil
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_html/get_by_xpath_all.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = X('//*[@id="root"]/div/main/div/*')
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = [
8 | 'Welcome to Ferret E2E test page!
',
9 | 'It has several pages for testing different possibilities of the library
'
10 | ]
11 | LET actual = INNER_HTML_ALL(doc, selector)
12 |
13 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/element_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-element-content"
5 | LET btnSelector = "#wait-element-btn"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 |
9 | CLICK(doc, btnSelector)
10 |
11 | WAIT_ELEMENT(doc, elemSelector, 10000)
12 |
13 | T::TRUE(ELEMENT_EXISTS(doc, elemSelector), "element not found")
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/iframes/input.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/forms"
2 | LET page = DOCUMENT(url, true)
3 |
4 | LET frames = (
5 | FOR f IN page.frames
6 | FILTER f.name == "nested"
7 | LIMIT 1
8 | RETURN f
9 | )
10 | LET doc = FIRST(frames)
11 |
12 | WAIT_ELEMENT(doc, "form")
13 |
14 | LET output = ELEMENT(doc, "#text_output")
15 |
16 | INPUT(doc, "#text_input", "foo")
17 |
18 | RETURN T::EQ(output.innerText, "foo")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_text/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 |
4 | LET expected = `Ferret
5 | Forms
6 | Navigation
7 | Events
8 | iFrame
9 | Welcome to Ferret E2E test page!
10 | It has several pages for testing different possibilities of the library
11 | `
12 | LET actual = INNER_TEXT(doc)
13 |
14 | LET r1 = '(\n|\s)'
15 | LET r2 = '(\n|\s)'
16 |
17 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/iframes/hover.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/events"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET doc = page.frames[1]
4 |
5 | WAIT_ELEMENT(doc, "#page-events", 5000)
6 |
7 | LET input = ELEMENT(doc, "#hoverable-btn")
8 |
9 | HOVER(input)
10 | WAIT_ELEMENT(doc, "#hoverable-content")
11 |
12 | LET output = ELEMENT(doc, "#hoverable-content")
13 |
14 | RETURN T::EQ(output.innerText, "Lorem ipsum dolor sit amet.")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/styles/set_2.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 |
6 | WAIT_ELEMENT(doc, pageSelector)
7 |
8 | LET el = ELEMENT(doc, elemSelector)
9 | LET prev = el.style
10 |
11 | STYLE_SET(el, "color", "black")
12 |
13 | WAIT(1000)
14 |
15 | LET curr = el.style
16 |
17 | T::EQ(curr.color, "rgb(0, 0, 0)")
18 |
19 | RETURN NONE
--------------------------------------------------------------------------------
/examples/history-api.fql:
--------------------------------------------------------------------------------
1 | LET page = DOCUMENT("https://soundcloud.com", { driver: "cdp"})
2 | LET doc = page.mainFrame
3 |
4 | WAIT_ELEMENT(doc, ".trendingTracks")
5 | SCROLL_ELEMENT(doc, ".trendingTracks")
6 | WAIT_ELEMENT(doc, ".trendingTracks .badgeList__item")
7 |
8 | LET song = ELEMENT(doc, ".trendingTracks .badgeList__item")
9 | CLICK(song)
10 |
11 | WAIT_ELEMENT(doc, ".l-listen-hero")
12 |
13 | RETURN {
14 | current: page.url,
15 | first: doc.url
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/stdlib/io/net/lib.go:
--------------------------------------------------------------------------------
1 | package net
2 |
3 | import (
4 | "github.com/MontFerret/ferret/pkg/runtime/core"
5 | "github.com/MontFerret/ferret/pkg/stdlib/io/net/http"
6 | )
7 |
8 | // RegisterLib register `NET` namespace functions.
9 | // @namespace NET
10 | func RegisterLib(ns core.Namespace) error {
11 | io := ns.Namespace("NET")
12 |
13 | if err := http.RegisterLib(io); err != nil {
14 | return core.Error(err, "register `HTTP`")
15 | }
16 |
17 | return nil
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_class_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | CLICK(doc, "#wait-no-class-btn")
8 | WAIT_NO_CLASS(doc, "#wait-no-class-content", "alert-success")
9 |
10 | // with random timeout
11 | CLICK(doc, "#wait-no-class-random-btn")
12 | WAIT_NO_CLASS(doc, "#wait-no-class-random-content", "alert-success", 10000)
13 |
14 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/inner_html/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET el = ELEMENT(doc, "#index")
4 |
5 | LET expected = "This node was injected by Ferret"
6 | INNER_HTML_SET(el, "This node was injected by Ferret")
7 | LET actual = INNER_HTML(el)
8 |
9 | WAIT(100)
10 |
11 | LET r1 = '(\s|\")'
12 | LET r2 = '(\n|\s|\")'
13 |
14 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_html/get_by_xpath_all.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = [
5 | 'Containers',
6 | 'Responsive breakpoints',
7 | 'Z-index'
8 | ]
9 | LET actual = (
10 | FOR i IN INNER_HTML_ALL(doc, X("//*[contains(@class, 'section-nav')]/li"))
11 | RETURN TRIM(i)
12 | )
13 |
14 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/class_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | CLICK(doc, "#wait-class-btn")
8 | WAIT_CLASS(doc, X("//*[@id='wait-class-content']"), "alert-success")
9 |
10 | // with random timeout
11 | CLICK(doc, "#wait-class-random-btn")
12 | WAIT_CLASS(doc, X("//*[@id='wait-class-random-content']"), "alert-success", 10000)
13 |
14 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/runtime/expressions/member_path.go:
--------------------------------------------------------------------------------
1 | package expressions
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | type MemberPathSegment struct {
6 | exp core.Expression
7 | optional bool
8 | }
9 |
10 | func NewMemberPathSegment(source core.Expression, optional bool) (*MemberPathSegment, error) {
11 | if source == nil {
12 | return nil, core.Error(core.ErrMissedArgument, "source")
13 | }
14 |
15 | return &MemberPathSegment{source, optional}, nil
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/runtime/values/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | var (
6 | None = core.NewType("none")
7 | Boolean = core.NewType("boolean")
8 | Int = core.NewType("int")
9 | Float = core.NewType("float")
10 | String = core.NewType("string")
11 | DateTime = core.NewType("date_time")
12 | Array = core.NewType("array")
13 | Object = core.NewType("object")
14 | Binary = core.NewType("binary")
15 | )
16 |
--------------------------------------------------------------------------------
/e2e/pages/dynamic/components/pages/index.js:
--------------------------------------------------------------------------------
1 | const e = React.createElement;
2 |
3 | export default function IndexPage() {
4 | return e("div", { className: "jumbotron", "data-type": "page", id: "index" }, [
5 | e("div", null,
6 | e("h1", null, "Welcome to Ferret E2E test page!")
7 | ),
8 | e("div", null,
9 | e("p", { className: "lead" }, "It has several pages for testing different possibilities of the library")
10 | )
11 | ])
12 | }
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/attr_all_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn")
7 | T::LEN(ELEMENTS(doc, X("//*[@id='wait-class-content' or @id='wait-class-random-content']")), 2)
8 | WAIT_ATTR_ALL(doc, X("//*[@id='wait-class-content' or @id='wait-class-random-content']"), "class", "alert alert-success", 10000)
9 |
10 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_element_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-element-content"
5 | LET btnSelector = "#wait-no-element-btn"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 |
9 | CLICK(doc, btnSelector)
10 |
11 | WAIT_NO_ELEMENT(doc, elemSelector, 10000)
12 |
13 | T::FALSE(ELEMENT_EXISTS(doc, elemSelector), "element should not be found")
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/style_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = "#wait-class-btn"
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | LET el = ELEMENT(doc, selector)
8 | LET prev = el.style
9 |
10 | ATTR_SET(el, "style", "width: 200px")
11 | WAIT_STYLE(doc, selector, "width", "200px")
12 |
13 | LET curr = el.style
14 |
15 | T::NOT::EQ(prev.width, "200px")
16 | T::EQ(curr.width, "200px")
17 |
18 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/element_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = X("//*[@id='page-events']")
4 | LET elemSelector = X("//*[@id='wait-element-content']")
5 | LET btnSelector = "#wait-element-btn"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 |
9 | CLICK(doc, btnSelector)
10 |
11 | WAIT_ELEMENT(doc, elemSelector, 10000)
12 |
13 | T::TRUE(ELEMENT_EXISTS(doc, elemSelector), "element not found")
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/runtime/expressions/literals/float.go:
--------------------------------------------------------------------------------
1 | package literals
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | type FloatLiteral float64
11 |
12 | func NewFloatLiteral(value float64) FloatLiteral {
13 | return FloatLiteral(value)
14 | }
15 |
16 | func (l FloatLiteral) Exec(_ context.Context, _ *core.Scope) (core.Value, error) {
17 | return values.NewFloat(float64(l)), nil
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/runtime/expressions/literals/string.go:
--------------------------------------------------------------------------------
1 | package literals
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | type StringLiteral string
11 |
12 | func NewStringLiteral(str string) StringLiteral {
13 | return StringLiteral(str)
14 | }
15 |
16 | func (l StringLiteral) Exec(_ context.Context, _ *core.Scope) (core.Value, error) {
17 | return values.NewString(string(l)), nil
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_html/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/simple.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = `
5 |
6 |
7 |
8 | Title
9 |
10 |
11 | Hello world
12 |
13 | `
14 | LET actual = INNER_HTML(doc)
15 |
16 | LET r1 = '(\s|\")'
17 | LET r2 = '(\n|\s|\")'
18 |
19 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/viewport/size.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/media"
2 | LET expectedW = 1920
3 | LET expectedH = 1080
4 |
5 | LET doc = DOCUMENT(url, {
6 | driver: 'cdp',
7 | viewport: {
8 | width: expectedW,
9 | height: expectedH
10 | }
11 | })
12 |
13 | LET actualW = TO_INT(INNER_TEXT(doc, '#screen-width'))
14 | LET actualH = TO_INT(INNER_TEXT(doc, '#screen-height'))
15 |
16 | T::EQ(actualW, expectedW)
17 | T::EQ(actualH, expectedH)
18 |
19 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_element_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = X("//*[@id='wait-no-element-content']")
5 | LET btnSelector = "#wait-no-element-btn"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 |
9 | CLICK(doc, btnSelector)
10 |
11 | WAIT_NO_ELEMENT(doc, elemSelector, 10000)
12 |
13 | T::FALSE(ELEMENT_EXISTS(doc, elemSelector), "element should not be found")
14 |
15 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/style_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = X("//*[@id='wait-class-btn']")
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | LET el = ELEMENT(doc, selector)
8 | LET prev = el.style
9 |
10 | ATTR_SET(el, "style", "width: 200px")
11 | WAIT_STYLE(doc, selector, "width", "200px")
12 |
13 | LET curr = el.style
14 |
15 | T::NOT::EQ(prev.width, "200px")
16 | T::EQ(curr.width, "200px")
17 |
18 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_html/get_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = 'ContainersResponsive breakpointsZ-index'
5 | LET actual = INNER_HTML(doc, '.section-nav')
6 |
7 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/pkg/stdlib/math/pi.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "context"
5 | "math"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | )
10 |
11 | // PI returns Pi value.
12 | // @return {Float} - Pi value.
13 | func Pi(_ context.Context, args ...core.Value) (core.Value, error) {
14 | err := core.ValidateArgs(args, 0, 0)
15 |
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | return values.NewFloat(math.Pi), nil
21 | }
22 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_html/get_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = '#root > div > main > div'
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = 'Welcome to Ferret E2E test page!
It has several pages for testing different possibilities of the library
'
8 | LET actual = INNER_HTML(doc, selector)
9 |
10 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_class_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | CLICK(doc, "#wait-no-class-btn")
8 | WAIT_NO_CLASS(doc, X("//*[@id='wait-no-class-content']"), "alert-success")
9 |
10 | // with random timeout
11 | CLICK(doc, "#wait-no-class-random-btn")
12 | WAIT_NO_CLASS(doc, X("//*[@id='wait-no-class-random-content']"), "alert-success", 10000)
13 |
14 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/io/net/http/lib.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | // RegisterLib register `HTTP` namespace functions.
6 | // @namespace HTTP
7 | func RegisterLib(ns core.Namespace) error {
8 | return ns.
9 | Namespace("HTTP").
10 | RegisterFunctions(
11 | core.NewFunctionsFromMap(map[string]core.Function{
12 | "GET": GET,
13 | "POST": POST,
14 | "PUT": PUT,
15 | "DELETE": DELETE,
16 | "DO": REQUEST,
17 | }))
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/stdlib/objects/lib.go:
--------------------------------------------------------------------------------
1 | package objects
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | func RegisterLib(ns core.Namespace) error {
6 | return ns.RegisterFunctions(
7 | core.NewFunctionsFromMap(map[string]core.Function{
8 | "HAS": Has,
9 | "KEYS": Keys,
10 | "KEEP_KEYS": KeepKeys,
11 | "MERGE": Merge,
12 | "ZIP": Zip,
13 | "VALUES": Values,
14 | "MERGE_RECURSIVE": MergeRecursive,
15 | }))
16 | }
17 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/waitfor_event/frame_navigation.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/iframe"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET original = FIRST(FRAMES(page, "name", "nested"))
4 |
5 | INPUT(original, "#url_input", "https://getbootstrap.com/")
6 | CLICK(original, "#submit")
7 |
8 | WAITFOR EVENT "navigation" IN page OPTIONS { frame: original }
9 |
10 | LET current = FIRST(FRAMES(page, "name", "nested"))
11 |
12 | RETURN T::EQ(current.URL, "https://getbootstrap.com/")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/styles/remove.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 |
6 | WAIT_ELEMENT(doc, pageSelector)
7 | LET el = ELEMENT(doc, elemSelector)
8 |
9 | LET prev = el.attributes.style
10 |
11 | STYLE_REMOVE(el, "display")
12 |
13 | LET curr = el.attributes.style
14 |
15 | T::EQ(prev.display, "block")
16 | T::EQ(curr.display, "inline")
17 |
18 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/cookies/delete.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, {
3 | driver: "cdp",
4 | cookies: [{
5 | name: "x-e2e",
6 | value: "test"
7 | }, {
8 | name: "x-e2e-2",
9 | value: "test2"
10 | }]
11 | })
12 |
13 | COOKIE_DEL(doc, COOKIE_GET(doc, "x-e2e"), "x-e2e-2")
14 |
15 | LET cookie1 = COOKIE_GET(doc, "x-e2e")
16 | LET cookie2 = COOKIE_GET(doc, "x-e2e-2")
17 |
18 | T::EQ(cookie1, NONE)
19 | T::EQ(cookie2, NONE)
20 |
21 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/headers/default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "http_headers"
4 | })
5 |
6 | LET el = ELEMENT(page, "#headers")
7 | LET actual = JSON_PARSE(el.innerText)
8 |
9 | LET expected = {
10 | "Single_header": ["single_header_value"],
11 | "Multi_set_header":["multi_set_header_value"],
12 | }
13 |
14 | RETURN T::EQ({
15 | "Single_header": actual["Single_header"],
16 | "Multi_set_header": actual["Multi_set_header"],
17 | }, expected)
--------------------------------------------------------------------------------
/pkg/compiler/result.go:
--------------------------------------------------------------------------------
1 | package compiler
2 |
3 | type visitorFn func() (interface{}, error)
4 |
5 | type result struct {
6 | data interface{}
7 | err error
8 | }
9 |
10 | func newResultFrom(fn visitorFn) *result {
11 | out, err := fn()
12 |
13 | return &result{out, err}
14 | }
15 |
16 | func (res *result) Ok() bool {
17 | return res.err == nil
18 | }
19 |
20 | func (res *result) Data() interface{} {
21 | return res.data
22 | }
23 |
24 | func (res *result) Error() error {
25 | return res.err
26 | }
27 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/headers/default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "cdp_headers"
4 | })
5 |
6 | LET el = ELEMENT(page, "#headers")
7 | LET actual = JSON_PARSE(el.innerText)
8 |
9 | LET expected = {
10 | "Single_header": ["single_header_value"],
11 | "Multi_set_header":["multi_set_header_value"],
12 | }
13 |
14 | RETURN T::EQ({
15 | "Single_header": actual["Single_header"],
16 | "Multi_set_header": actual["Multi_set_header"],
17 | }, expected)
--------------------------------------------------------------------------------
/pkg/compiler/errors.go:
--------------------------------------------------------------------------------
1 | package compiler
2 |
3 | import "github.com/pkg/errors"
4 |
5 | var (
6 | ErrEmptyQuery = errors.New("empty query")
7 | ErrNotImplemented = errors.New("not implemented")
8 | ErrVariableNotFound = errors.New("variable not found")
9 | ErrVariableNotUnique = errors.New("variable is already defined")
10 | ErrInvalidToken = errors.New("invalid token")
11 | ErrUnexpectedToken = errors.New("unexpected token")
12 | ErrInvalidDataSource = errors.New("invalid data source")
13 | )
14 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_text/get_by_css_all.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = '#root > div > main > div > *'
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = [
8 | 'Welcome to Ferret E2E test page!',
9 | 'It has several pages for testing different possibilities of the library'
10 | ]
11 | LET actual = (
12 | FOR str IN INNER_TEXT_ALL(doc, selector)
13 | RETURN REGEX_REPLACE(TRIM(str), '\n', '')
14 | )
15 |
16 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/pkg/runtime/expressions/literals/none.go:
--------------------------------------------------------------------------------
1 | package literals
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | type noneLiteral struct{}
11 |
12 | var None = &noneLiteral{}
13 |
14 | func (l noneLiteral) Exec(_ context.Context, _ *core.Scope) (core.Value, error) {
15 | return values.None, nil
16 | }
17 |
18 | func IsNone(exp core.Expression) bool {
19 | _, is := exp.(*noneLiteral)
20 |
21 | return is
22 | }
23 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_html/get_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, { driver: "cdp" })
3 | LET selector = X('//*[@id="root"]/div/main/div')
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = 'Welcome to Ferret E2E test page!
It has several pages for testing different possibilities of the library
'
8 | LET actual = INNER_HTML(doc, selector)
9 |
10 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/pkg/runtime/core/source.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "fmt"
4 |
5 | type SourceMap struct {
6 | text string
7 | line int
8 | column int
9 | }
10 |
11 | func NewSourceMap(text string, line, col int) SourceMap {
12 | return SourceMap{text, line, col}
13 | }
14 |
15 | func (s SourceMap) Line() int {
16 | return s.line
17 | }
18 |
19 | func (s SourceMap) Column() int {
20 | return s.column
21 | }
22 |
23 | func (s SourceMap) String() string {
24 | return fmt.Sprintf("%s at %d:%d", s.text, s.line, s.column)
25 | }
26 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/inner_text/get_by_xpath_all.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = X('.//*[@id="root"]/div/main/div/*')
4 |
5 | WAIT_ELEMENT(doc, "#layout")
6 |
7 | LET expected = [
8 | 'Welcome to Ferret E2E test page!',
9 | 'It has several pages for testing different possibilities of the library'
10 | ]
11 | LET actual = (
12 | FOR str IN INNER_TEXT_ALL(doc, selector)
13 | RETURN REGEX_REPLACE(TRIM(str), '\n', '')
14 | )
15 |
16 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/e2e/tests/static/doc/inner_html/get_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/overview.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = 'ContainersResponsive breakpointsZ-index'
5 | LET actual = INNER_HTML(doc, X("//[contains(@class, 'section-nav')]"))
6 |
7 | RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', ''))
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/styles/set_many.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 |
6 | WAIT_ELEMENT(doc, pageSelector)
7 |
8 | LET el = ELEMENT(doc, elemSelector)
9 | LET prev = el.style
10 |
11 | STYLE_SET(el, { color: "black", "font-size": "10px"})
12 |
13 | WAIT(1000)
14 |
15 | LET curr = el.style
16 |
17 | T::EQ(curr.color, "rgb(0, 0, 0)")
18 | T::EQ(curr["font-size"], "10px")
19 |
20 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/runtime/expressions/literals/boolean.go:
--------------------------------------------------------------------------------
1 | package literals
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | type BooleanLiteral bool
11 |
12 | func NewBooleanLiteral(val bool) BooleanLiteral {
13 | return BooleanLiteral(val)
14 | }
15 |
16 | func (l BooleanLiteral) Exec(_ context.Context, _ *core.Scope) (core.Value, error) {
17 | if l {
18 | return values.True, nil
19 | }
20 |
21 | return values.False, nil
22 | }
23 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/attrs/set_many.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 | LET color = "rgb(66, 66, 66)"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 |
9 | LET el = ELEMENT(doc, elemSelector)
10 |
11 | ATTR_SET(el, {
12 | style: "color:" + color,
13 | "data-ferret-x": "test"
14 | })
15 |
16 | T::EQ(el.attributes.style.color, color)
17 | T::EQ(el.attributes["data-ferret-x"], "test")
18 |
19 | RETURN TRUE
--------------------------------------------------------------------------------
/examples/pagination_while.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://github.com/marketplace/category/api-management", { driver: "cdp"})
2 | LET nextSelector = ".paginate-container .BtnGroup a:nth-child(2)"
3 | LET elementsSelector = '[data-hydro-click]'
4 |
5 | WAIT_ELEMENT(doc, elementsSelector)
6 |
7 | FOR i DO WHILE ELEMENT_EXISTS(doc, nextSelector)
8 | LET wait = i > 0 ? CLICK(doc, nextSelector) : false
9 | LET nav = wait ? WAIT(2000) && WAIT_ELEMENT(doc, elementsSelector) : false
10 |
11 | FOR el IN ELEMENTS(doc, elementsSelector)
12 | RETURN el
13 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/iframes/hover.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/events"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET frames = (
4 | FOR f IN page.frames
5 | FILTER f.name == "nested"
6 | LIMIT 1
7 | RETURN f
8 | )
9 | LET doc = FIRST(frames)
10 |
11 | WAIT_ELEMENT(doc, "#page-events")
12 |
13 | HOVER(doc, "#hoverable-btn")
14 | WAIT_ELEMENT(doc, "#hoverable-content")
15 |
16 | LET output = INNER_TEXT(doc, "#hoverable-content")
17 |
18 | RETURN T::EQ(output, "Lorem ipsum dolor sit amet.")
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/attrs/remove.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 | LET attrName = "data-e2e-test"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 | LET el = ELEMENT(doc, elemSelector)
9 | ATTR_SET(el, attrName, "true")
10 |
11 | LET prev = el.attributes[attrName]
12 |
13 | ATTR_REMOVE(el, attrName)
14 |
15 | LET curr = el.attributes[attrName]
16 |
17 | T::EQ(prev, "true")
18 | T::NONE(curr)
19 |
20 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/frame_navigation.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/iframe"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET original = FIRST(FRAMES(page, "url", "/\?redirect=/iframe$"))
4 |
5 | INPUT(original, "#url_input", "https://getbootstrap.com/")
6 |
7 | CLICK(original, "#submit")
8 |
9 | WAITFOR EVENT "navigation" IN page
10 | FILTER original == current.frame
11 | TIMEOUT 10000
12 |
13 | LET current = FIRST(FRAMES(page, "name", "nested"))
14 |
15 | RETURN T::EQ(current.URL, "https://getbootstrap.com/")
--------------------------------------------------------------------------------
/pkg/drivers/cdp/network/options.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "regexp"
5 |
6 | "github.com/mafredri/cdp/protocol/page"
7 |
8 | "github.com/MontFerret/ferret/pkg/drivers"
9 | )
10 |
11 | type (
12 | Cookies map[string]*drivers.HTTPCookies
13 |
14 | Filter struct {
15 | Patterns []drivers.ResourceFilter
16 | }
17 |
18 | Options struct {
19 | Cookies Cookies
20 | Headers *drivers.HTTPHeaders
21 | Filter *Filter
22 | }
23 |
24 | WaitEventOptions struct {
25 | FrameID page.FrameID
26 | URL *regexp.Regexp
27 | }
28 | )
29 |
--------------------------------------------------------------------------------
/pkg/runtime/core/expression.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "context"
4 |
5 | type (
6 | Expression interface {
7 | Exec(ctx context.Context, scope *Scope) (Value, error)
8 | }
9 |
10 | ExpressionFn struct {
11 | fn func(ctx context.Context, scope *Scope) (Value, error)
12 | }
13 | )
14 |
15 | func AsExpression(fn func(ctx context.Context, scope *Scope) (Value, error)) Expression {
16 | return &ExpressionFn{fn}
17 | }
18 |
19 | func (f *ExpressionFn) Exec(ctx context.Context, scope *Scope) (Value, error) {
20 | return f.fn(ctx, scope)
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/fail.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
8 | )
9 |
10 | // FAIL returns an error.
11 | // @param {String} [message] - Message to display on error.
12 | var Fail = base.Assertion{
13 | DefaultMessage: func(_ []core.Value) string {
14 | return "not fail"
15 | },
16 | MinArgs: 0,
17 | MaxArgs: 1,
18 | Fn: func(_ context.Context, _ []core.Value) (bool, error) {
19 | return false, nil
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/no_style.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | LET el = ELEMENT(doc, "#wait-class-content")
7 |
8 | ATTR_SET(el, "style", "color: black")
9 | WAIT_STYLE(el, "color", "rgb(0, 0, 0)")
10 |
11 | LET prev = el.style
12 |
13 | ATTR_SET(el, "style", "color: red")
14 | WAIT_NO_STYLE(el, "color", "black")
15 |
16 | LET curr = el.style
17 |
18 | T::EQ(prev.color, "rgb(0, 0, 0)")
19 | T::EQ(curr.color, "rgb(255, 0, 0)")
20 |
21 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/now.go:
--------------------------------------------------------------------------------
1 | package datetime
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/values"
7 |
8 | "github.com/MontFerret/ferret/pkg/runtime/core"
9 | )
10 |
11 | // NOW returns new DateTime object with Time equal to time.Now().
12 | // @return {DateTime} - New DateTime object.
13 | func Now(_ context.Context, args ...core.Value) (core.Value, error) {
14 | err := core.ValidateArgs(args, 0, 0)
15 | if err != nil {
16 | return values.None, err
17 | }
18 |
19 | return values.NewCurrentDateTime(), nil
20 | }
21 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/class.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | LET b1 = ELEMENT(doc, "#wait-class-btn")
8 | LET c1 = ELEMENT(doc, "#wait-class-content")
9 |
10 | CLICK(b1)
11 | WAIT_CLASS(c1, "alert-success")
12 |
13 | // with random timeout
14 | LET b2 = ELEMENT(doc, "#wait-class-random-btn")
15 | LET c2 = ELEMENT(doc, "#wait-class-random-content")
16 |
17 | CLICK(b2)
18 | WAIT_CLASS(c2, "alert-success", 10000)
19 |
20 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/doc/cookies/default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "http_cookies"
4 | })
5 |
6 | LET el = ELEMENT(page, "#cookies")
7 | LET actual = (
8 | FOR c IN JSON_PARSE(el.innerText)
9 | SORT c.Name
10 | RETURN c
11 | )
12 |
13 | LET expected = {
14 | "Single_cookie": "single_cookie_value",
15 | "Multi_set_cookie": "multi_set_cookie_value",
16 | }
17 |
18 | RETURN T::EQ({
19 | "Single_cookie": actual[2].Value,
20 | "Multi_set_cookie": actual[0].Value,
21 | }, expected)
--------------------------------------------------------------------------------
/e2e/tests/static/headers/override_default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "http_headers",
4 | headers: {
5 | "single_header": "foo"
6 | }
7 | })
8 |
9 | LET el = ELEMENT(page, "#headers")
10 | LET actual = JSON_PARSE(el.innerText)
11 |
12 | LET expected = {
13 | "Single_header": ["foo"],
14 | "Multi_set_header":["multi_set_header_value"],
15 | }
16 |
17 | RETURN T::EQ({
18 | "Single_header": actual["Single_header"],
19 | "Multi_set_header": actual["Multi_set_header"],
20 | }, expected)
--------------------------------------------------------------------------------
/pkg/drivers/common/errors.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/rs/zerolog"
7 |
8 | "github.com/MontFerret/ferret/pkg/runtime/core"
9 | )
10 |
11 | var (
12 | ErrReadOnly = core.Error(core.ErrInvalidOperation, "read only")
13 | ErrInvalidPath = core.Error(core.ErrInvalidOperation, "invalid path")
14 | )
15 |
16 | func CloseAll(logger zerolog.Logger, closers []io.Closer, msg string) {
17 | for _, closer := range closers {
18 | if err := closer.Close(); err != nil {
19 | logger.Error().Err(err).Msg(msg)
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/attrs/set.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 | LET attrName = "data-e2e-test"
6 |
7 | WAIT_ELEMENT(doc, pageSelector)
8 |
9 | LET el = ELEMENT(doc, elemSelector)
10 | ATTR_SET(el, attrName, "true")
11 |
12 | LET prev = el.attributes[attrName]
13 |
14 | T::EQ(prev, "true")
15 |
16 | ATTR_SET(el, attrName, "false")
17 |
18 | LET curr = el.attributes[attrName]
19 |
20 | T::EQ(curr, "false")
21 |
22 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/headers/override_default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "cdp_headers",
4 | headers: {
5 | "single_header": "foo"
6 | }
7 | })
8 |
9 | LET el = ELEMENT(page, "#headers")
10 | LET actual = JSON_PARSE(el.innerText)
11 |
12 | LET expected = {
13 | "Single_header": ["foo"],
14 | "Multi_set_header":["multi_set_header_value"],
15 | }
16 |
17 | RETURN T::EQ({
18 | "Single_header": actual["Single_header"],
19 | "Multi_set_header": actual["Multi_set_header"],
20 | }, expected)
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/attr_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = "#wait-class-btn"
4 | LET attrName = "data-ferret-x"
5 | LET attrVal = "foobar"
6 |
7 | WAIT_ELEMENT(doc, "#page-events")
8 |
9 | LET el = ELEMENT(doc, selector)
10 | LET prev = el.attributes
11 |
12 | ATTR_SET(el, attrName, attrVal)
13 | WAIT_ATTR(doc, selector, attrName, attrVal, 30000)
14 |
15 | LET curr = el.attributes
16 |
17 | T::NONE(prev[attrName])
18 | T::EQ(attrVal, curr[attrName], "attributes should be updated")
19 |
20 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/clear/clear.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET input = ELEMENT(doc, "#text_input")
7 |
8 | INPUT(input, "Foo", 100)
9 |
10 | INPUT_CLEAR(input)
11 |
12 | T::EMPTY(INNER_TEXT(doc, "#text_output"))
13 |
14 | INPUT(input, "test0-test1", 100)
15 |
16 | INPUT_CLEAR(input)
17 |
18 | T::EMPTY(INNER_TEXT(doc, "#text_output"))
19 |
20 | INPUT(input, "test0&test1", 100)
21 |
22 | INPUT_CLEAR(input)
23 |
24 | T::EMPTY(INNER_TEXT(doc, "#text_output"))
25 |
26 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/runtime/core/source_test.go:
--------------------------------------------------------------------------------
1 | package core_test
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | . "github.com/smartystreets/goconvey/convey"
8 |
9 | "github.com/MontFerret/ferret/pkg/runtime/core"
10 | )
11 |
12 | func TestNewSourceMap(t *testing.T) {
13 | Convey("Should match", t, func() {
14 | s := core.NewSourceMap("test", 1, 100)
15 | sFmt := fmt.Sprintf("%s at %d:%d", "test", 1, 100)
16 |
17 | So(s, ShouldNotBeNil)
18 |
19 | So(s.Line(), ShouldEqual, 1)
20 |
21 | So(s.Column(), ShouldEqual, 100)
22 |
23 | So(s.String(), ShouldEqual, sFmt)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/stdlib/io/net/http/post.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "context"
5 | h "net/http"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | )
9 |
10 | // POST makes a POST request.
11 | // @param {Object} params - Request parameters.
12 | // @param {String} params.url - Target url
13 | // @param {Any} params.body - Request data
14 | // @param {Object} [params.headers] - HTTP headers
15 | // @return {Binary} - Response in binary format
16 | func POST(ctx context.Context, args ...core.Value) (core.Value, error) {
17 | return execMethod(ctx, h.MethodPost, args)
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/stdlib/io/net/http/put.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "context"
5 | h "net/http"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | )
9 |
10 | // PUT makes a PUT HTTP request.
11 | // @param {Object} params - Request parameters.
12 | // @param {String} params.url - Target url
13 | // @param {Any} params.body - Request data
14 | // @param {Object} [params.headers] - HTTP headers
15 | // @return {Binary} - Response in binary format
16 | func PUT(ctx context.Context, args ...core.Value) (core.Value, error) {
17 | return execMethod(ctx, h.MethodPut, args)
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_attr_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | CLICK(doc, "#wait-no-class-btn")
8 | WAIT(1000)
9 | PRINT(ATTR_GET(ELEMENT(doc, "#wait-no-class-content"), "class"))
10 | WAIT_NO_ATTR(doc, "#wait-no-class-content", "class", "alert alert-success")
11 |
12 | // with random timeout
13 | CLICK(doc, "#wait-no-class-random-btn")
14 | WAIT_NO_ATTR(doc, "#wait-no-class-random-content", "class", "alert alert-success", 10000)
15 |
16 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/no_class.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | LET b1 = ELEMENT(doc, "#wait-no-class-btn")
8 | LET c1 = ELEMENT(doc, "#wait-no-class-content")
9 |
10 | CLICK(b1)
11 | WAIT_NO_CLASS(c1, "alert-success")
12 |
13 | // with random timeout
14 | LET b2 = ELEMENT(doc, "#wait-no-class-random-btn")
15 | LET c2 = ELEMENT(doc, "#wait-no-class-random-content")
16 |
17 | CLICK(b2)
18 | WAIT_NO_CLASS(c2, "alert-success", 10000)
19 |
20 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/cookies/default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "cdp_cookies"
4 | })
5 |
6 | LET el = ELEMENT(page, "#cookies")
7 | LET actual = (
8 | FOR c IN JSON_PARSE(el.innerText)
9 | SORT c.Name
10 | RETURN c
11 | )
12 |
13 | LET expected = {
14 | "Single_cookie": "single_cookie_value",
15 | "Multi_set_cookie": "multi_set_cookie_value",
16 | }
17 |
18 | T::EQ({
19 | "Single_cookie": actual[2].Value,
20 | "Multi_set_cookie": actual[0].Value,
21 | }, expected)
22 |
23 |
24 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/path/lib.go:
--------------------------------------------------------------------------------
1 | package path
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | // RegisterLib register `PATH` namespace functions.
6 | // @namespace PATH
7 | func RegisterLib(ns core.Namespace) error {
8 | return ns.
9 | Namespace("PATH").
10 | RegisterFunctions(
11 | core.NewFunctionsFromMap(map[string]core.Function{
12 | "BASE": Base,
13 | "CLEAN": Clean,
14 | "DIR": Dir,
15 | "EXT": Ext,
16 | "IS_ABS": IsAbs,
17 | "JOIN": Join,
18 | "MATCH": Match,
19 | "SEPARATE": Separate,
20 | }))
21 | }
22 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/attr.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | LET b1 = ELEMENT(doc, "#wait-class-btn")
8 | LET c1 = ELEMENT(doc, "#wait-class-content")
9 |
10 | CLICK(b1)
11 | WAIT_ATTR(c1, "class", "alert alert-success")
12 |
13 | // with random timeout
14 | LET b2 = ELEMENT(doc, "#wait-class-random-btn")
15 | LET c2 = ELEMENT(doc, "#wait-class-random-content")
16 |
17 | CLICK(b2)
18 | WAIT_ATTR(c2, "class", "alert alert-success", 10000)
19 |
20 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/io/net/http/delete.go:
--------------------------------------------------------------------------------
1 | package http
2 |
3 | import (
4 | "context"
5 | h "net/http"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | )
9 |
10 | // DELETE makes a DELETE request.
11 | // @param {Object} params - Request parameters.
12 | // @param {String} params.url - Target url
13 | // @param {Binary} params.body - Request data
14 | // @param {Object} [params.headers] - HTTP headers
15 | // @return {Binary} - Response in binary format
16 | func DELETE(ctx context.Context, args ...core.Value) (core.Value, error) {
17 | return execMethod(ctx, h.MethodDelete, args)
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/attr_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = X("//*[@id='wait-class-btn']")
4 | LET attrName = "data-ferret-x"
5 | LET attrVal = "foobar"
6 |
7 | WAIT_ELEMENT(doc, "#page-events")
8 |
9 | LET el = ELEMENT(doc, selector)
10 | LET prev = el.attributes
11 |
12 | ATTR_SET(el, attrName, attrVal)
13 | WAIT_ATTR(doc, selector, attrName, attrVal, 30000)
14 |
15 | LET curr = el.attributes
16 |
17 | T::NONE(prev[attrName])
18 | T::EQ(attrVal, curr[attrName], "attributes should be updated")
19 |
20 | RETURN TRUE
--------------------------------------------------------------------------------
/options.go:
--------------------------------------------------------------------------------
1 | package ferret
2 |
3 | import "github.com/MontFerret/ferret/pkg/compiler"
4 |
5 | type (
6 | Options struct {
7 | compiler []compiler.Option
8 | }
9 |
10 | Option func(opts *Options)
11 | )
12 |
13 | func NewOptions(setters []Option) *Options {
14 | res := &Options{
15 | compiler: make([]compiler.Option, 0, 2),
16 | }
17 |
18 | for _, setter := range setters {
19 | setter(res)
20 | }
21 |
22 | return res
23 | }
24 |
25 | func WithoutStdlib() Option {
26 | return func(opts *Options) {
27 | opts.compiler = append(opts.compiler, compiler.WithoutStdlib())
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/templates/document.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
4 |
5 | const domReady = `() => {
6 | if (document.readyState === 'complete') {
7 | return true;
8 | }
9 |
10 | return null;
11 | }`
12 |
13 | func DOMReady() *eval.Function {
14 | return eval.F(domReady)
15 | }
16 |
17 | const getTitle = `() => document.title`
18 |
19 | func GetTitle() *eval.Function {
20 | return eval.F(getTitle)
21 | }
22 |
23 | const getDocument = `() => document`
24 |
25 | func GetDocument() *eval.Function {
26 | return eval.F(getDocument)
27 | }
28 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/style.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | LET el = ELEMENT(doc, "#wait-class-content")
7 | LET original = el.style.color
8 |
9 | ATTR_SET(el, "style", "color: black")
10 | WAIT_STYLE(el, "color", "rgb(0, 0, 0)")
11 |
12 | LET prev = el.style
13 |
14 | ATTR_REMOVE(el, "style")
15 | WAIT_STYLE(el, "color", original)
16 |
17 | LET curr = el.style
18 |
19 | T::EQ(prev.color, "rgb(0, 0, 0)")
20 | T::EQ(curr.color, original, "style should be returned to original")
21 |
22 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/static/headers/set.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "http",
4 | headers: {
5 | "Access-Control-Allow-Origin": "*",
6 | "X-Request-Id": "foobar"
7 | }
8 | })
9 |
10 | LET el = ELEMENT(page, "#headers")
11 | LET actual = JSON_PARSE(el.innerText)
12 | LET expected = {
13 | "Access-Control-Allow-Origin": ["*"],
14 | "X-Request-Id": ["foobar"]
15 | }
16 |
17 | RETURN T::EQ({
18 | "Access-Control-Allow-Origin": actual["Access-Control-Allow-Origin"],
19 | "X-Request-Id": actual["X-Request-Id"]
20 | }, expected)
--------------------------------------------------------------------------------
/pkg/stdlib/arrays/outersection.go:
--------------------------------------------------------------------------------
1 | package arrays
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | )
8 |
9 | // OUTERSECTION return the values that occur only once across all arrays specified.
10 | // The element order is random.
11 | // @param {Any[], repeated} arrays - An arbitrary number of arrays as multiple arguments (at least 2).
12 | // @return {Any[]} - A single array with only the elements that exist only once across all provided arrays.
13 | func Outersection(_ context.Context, args ...core.Value) (core.Value, error) {
14 | return sections(args, 1)
15 | }
16 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_attr_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | CLICK(doc, "#wait-no-class-btn")
8 | WAIT(1000)
9 | PRINT(ATTR_GET(ELEMENT(doc, "#wait-no-class-content"), "class"))
10 | WAIT_NO_ATTR(doc, X("//*[@id='wait-no-class-content']"), "class", "alert alert-success")
11 |
12 | // with random timeout
13 | CLICK(doc, "#wait-no-class-random-btn")
14 | WAIT_NO_ATTR(doc, X("//*[@id='wait-no-class-random-content']"), "class", "alert alert-success", 10000)
15 |
16 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/style_all_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = "#wait-class-btn, #wait-class-random-btn"
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | LET n = (
8 | FOR el IN ELEMENTS(doc, selector)
9 | ATTR_SET(el, "style", "width: 200px")
10 |
11 | RETURN NONE
12 | )
13 |
14 | WAIT_STYLE_ALL(doc, selector, "width", "200px", 10000)
15 |
16 | LET results = (
17 | FOR el IN ELEMENTS(doc, selector)
18 | RETURN el.style.width
19 | )
20 |
21 | T::EQ(results, ["200px","200px"])
22 |
23 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/wait/no_attr.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "#page-events")
5 |
6 | // with fixed timeout
7 | LET b1 = ELEMENT(doc, "#wait-no-class-btn")
8 | LET c1 = ELEMENT(doc, "#wait-no-class-content")
9 |
10 | CLICK(b1)
11 | WAIT_NO_ATTR(c1, "class", "alert alert-success")
12 |
13 | // with random timeout
14 | LET b2 = ELEMENT(doc, "#wait-no-class-random-btn")
15 | LET c2 = ELEMENT(doc, "#wait-no-class-random-content")
16 |
17 | CLICK(b2)
18 | WAIT_NO_ATTR(c2, "class", "alert alert-success", 10000)
19 |
20 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/headers/set.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "cdp",
4 | headers: {
5 | "Access-Control-Allow-Origin": "*",
6 | "X-Request-Id": "foobar"
7 | }
8 | })
9 |
10 | LET el = ELEMENT(page, "#headers")
11 | LET actual = JSON_PARSE(el.innerText)
12 | LET expected = {
13 | "Access-Control-Allow-Origin": ["*"],
14 | "X-Request-Id": ["foobar"]
15 | }
16 |
17 | RETURN T::EQ({
18 | "Access-Control-Allow-Origin": actual["Access-Control-Allow-Origin"],
19 | "X-Request-Id": actual["X-Request-Id"]
20 | }, expected)
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/now_test.go:
--------------------------------------------------------------------------------
1 | package datetime_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/MontFerret/ferret/pkg/stdlib/datetime"
7 |
8 | "github.com/MontFerret/ferret/pkg/runtime/core"
9 | "github.com/MontFerret/ferret/pkg/runtime/values"
10 | )
11 |
12 | func TestNow(t *testing.T) {
13 | tcs := []*testCase{
14 | &testCase{
15 | Name: "When too many arguments",
16 | Expected: values.None,
17 | Args: []core.Value{
18 | values.NewCurrentDateTime(),
19 | },
20 | ShouldErr: true,
21 | },
22 | }
23 |
24 | for _, tc := range tcs {
25 | tc.Do(t, datetime.Now)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/stdlib/io/lib.go:
--------------------------------------------------------------------------------
1 | package io
2 |
3 | import (
4 | "github.com/MontFerret/ferret/pkg/runtime/core"
5 | "github.com/MontFerret/ferret/pkg/stdlib/io/fs"
6 | "github.com/MontFerret/ferret/pkg/stdlib/io/net"
7 | )
8 |
9 | // RegisterLib register `IO` namespace functions.
10 | // @namespace IO
11 | func RegisterLib(ns core.Namespace) error {
12 | io := ns.Namespace("IO")
13 |
14 | if err := fs.RegisterLib(io); err != nil {
15 | return core.Error(err, "register `FS`")
16 | }
17 |
18 | if err := net.RegisterLib(io); err != nil {
19 | return core.Error(err, "register `NET`")
20 | }
21 |
22 | return nil
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/templates/siblings.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "github.com/mafredri/cdp/protocol/runtime"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
7 | )
8 |
9 | const getPreviousElementSibling = "(el) => el.previousElementSibling"
10 | const getNextElementSibling = "(el) => el.nextElementSibling"
11 |
12 | func GetPreviousElementSibling(id runtime.RemoteObjectID) *eval.Function {
13 | return eval.F(getPreviousElementSibling).WithArgRef(id)
14 | }
15 |
16 | func GetNextElementSibling(id runtime.RemoteObjectID) *eval.Function {
17 | return eval.F(getNextElementSibling).WithArgRef(id)
18 | }
19 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/iframes/wait_class.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/events"
2 | LET page = DOCUMENT(url, true)
3 | LET frames = (
4 | FOR f IN page.frames
5 | FILTER f.name == "nested"
6 | LIMIT 1
7 | RETURN f
8 | )
9 | LET doc = FIRST(frames)
10 |
11 | WAIT_ELEMENT(doc, "#page-events")
12 |
13 | // with fixed timeout
14 | CLICK(doc, "#wait-class-btn")
15 | WAIT_CLASS(doc, "#wait-class-content", "alert-success")
16 |
17 | // with random timeout
18 | CLICK(doc, "#wait-class-random-btn")
19 | WAIT_CLASS(doc, "#wait-class-random-content", "alert-success", 10000)
20 |
21 | RETURN ""
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/style_all_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = X("//*[@id='wait-class-btn' or @id='wait-class-random-btn']")
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | LET n = (
8 | FOR el IN ELEMENTS(doc, selector)
9 | ATTR_SET(el, "style", "width: 200px")
10 |
11 | RETURN NONE
12 | )
13 |
14 | WAIT_STYLE_ALL(doc, selector, "width", "200px", 10000)
15 |
16 | LET results = (
17 | FOR el IN ELEMENTS(doc, selector)
18 | RETURN el.style.width
19 | )
20 |
21 | T::EQ(results, ["200px","200px"])
22 |
23 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/drivers/cdp/utils/layout.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "github.com/mafredri/cdp/protocol/page"
4 |
5 | func GetLayoutViewportWH(metrics *page.GetLayoutMetricsReply) (width int, height int) {
6 | if metrics.CSSLayoutViewport.ClientWidth > 0 {
7 | width = metrics.CSSLayoutViewport.ClientWidth
8 | } else {
9 | // Chrome version <=89
10 | width = metrics.LayoutViewport.ClientWidth
11 | }
12 |
13 | if metrics.CSSLayoutViewport.ClientHeight > 0 {
14 | height = metrics.CSSLayoutViewport.ClientHeight
15 | } else {
16 | // Chrome version <=89
17 | height = metrics.LayoutViewport.ClientHeight
18 | }
19 |
20 | return
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/type_name.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // TYPENAME returns the data type name of value.
11 | // @param {Any} value - Input value of arbitrary type.
12 | // @return {Boolean} - Returns string representation of a type.
13 | func TypeName(_ context.Context, args ...core.Value) (core.Value, error) {
14 | err := core.ValidateArgs(args, 1, 1)
15 |
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | return values.NewString(args[0].Type().String()), nil
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/eval/helpers.go:
--------------------------------------------------------------------------------
1 | package eval
2 |
3 | import (
4 | "strings"
5 |
6 | "github.com/mafredri/cdp/protocol/runtime"
7 |
8 | "github.com/MontFerret/ferret/pkg/drivers"
9 | "github.com/MontFerret/ferret/pkg/runtime/core"
10 | )
11 |
12 | func parseRuntimeException(details *runtime.ExceptionDetails) error {
13 | if details == nil || details.Exception == nil {
14 | return nil
15 | }
16 |
17 | desc := *details.Exception.Description
18 |
19 | if strings.Contains(desc, drivers.ErrNotFound.Error()) {
20 | return drivers.ErrNotFound
21 | }
22 |
23 | return core.Error(
24 | core.ErrUnexpected,
25 | desc,
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/iframes/wait_class.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/events"
2 | LET page = DOCUMENT(url, { driver: 'cdp' })
3 | LET doc = page.frames[1]
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | // with fixed timeout
8 | LET b1 = ELEMENT(doc, "#wait-class-btn")
9 | LET c1 = ELEMENT(doc, "#wait-class-content")
10 |
11 | WAIT(2000)
12 | CLICK(b1)
13 |
14 | WAIT_CLASS(c1, "alert-success")
15 |
16 | // with random timeout
17 | LET b2 = ELEMENT(doc, "#wait-class-random-btn")
18 | LET c2 = ELEMENT(doc, "#wait-class-random-content")
19 |
20 | CLICK(b2)
21 | WAIT_CLASS(c2, "alert-success", 10000)
22 |
23 | RETURN ""
--------------------------------------------------------------------------------
/pkg/drivers/cdp/templates/value.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "github.com/mafredri/cdp/protocol/runtime"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | )
9 |
10 | const getValue = `(el) => {
11 | return el.value
12 | }`
13 |
14 | func GetValue(id runtime.RemoteObjectID) *eval.Function {
15 | return eval.F(getValue).WithArgRef(id)
16 | }
17 |
18 | const setValue = `(el, value) => {
19 | el.value = value
20 | }`
21 |
22 | func SetValue(id runtime.RemoteObjectID, value core.Value) *eval.Function {
23 | return eval.F(setValue).WithArgRef(id).WithArgValue(value)
24 | }
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/sqrt_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestSqrt(t *testing.T) {
14 | Convey("Should return square value", t, func() {
15 | out, err := math.Sqrt(context.Background(), values.NewFloat(9))
16 |
17 | So(err, ShouldBeNil)
18 | So(out, ShouldEqual, 3)
19 |
20 | out, err = math.Sqrt(context.Background(), values.NewInt(2))
21 |
22 | So(err, ShouldBeNil)
23 | So(out, ShouldEqual, 1.4142135623730951)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/to_date_time.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // TO_DATETIME takes an input value of any type and converts it into the appropriate date time value.
11 | // @param {Any} value - Input value of arbitrary type.
12 | // @return {DateTime} - Parsed date time.
13 | func ToDateTime(_ context.Context, args ...core.Value) (core.Value, error) {
14 | err := core.ValidateArgs(args, 1, 1)
15 |
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | return values.ParseDateTime(args[0].String())
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/to_string.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // TO_STRING takes an input value of any type and convert it into a string value.
11 | // @param {Any} value - Input value of arbitrary type.
12 | // @return {String} - String representation of a given value.
13 | func ToString(_ context.Context, args ...core.Value) (core.Value, error) {
14 | err := core.ValidateArgs(args, 1, 1)
15 |
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | return values.NewString(args[0].String()), nil
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/to_binary.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // ToBinary takes an input value of any type and converts it into a binary value.
11 | // @param {Any} value - Input value of arbitrary type.
12 | // @return {Binary} - A binary value.
13 | func ToBinary(_ context.Context, args ...core.Value) (core.Value, error) {
14 | err := core.ValidateArgs(args, 1, 1)
15 |
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | val := args[0].String()
21 |
22 | return values.NewBinary([]byte(val)), nil
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/html/xpath_selector.go:
--------------------------------------------------------------------------------
1 | package html
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers"
7 |
8 | "github.com/MontFerret/ferret/pkg/runtime/core"
9 | "github.com/MontFerret/ferret/pkg/runtime/values"
10 | )
11 |
12 | // X returns QuerySelector of XPath kind.
13 | // @param {String} expression - XPath expression.
14 | // @return {Any} - Returns QuerySelector of XPath kind.
15 | func XPathSelector(_ context.Context, args ...core.Value) (core.Value, error) {
16 | if err := core.ValidateArgs(args, 1, 1); err != nil {
17 | return values.None, err
18 | }
19 |
20 | return drivers.NewXPathSelector(values.ToString(args[0])), nil
21 | }
22 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_style_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = "#wait-class-btn"
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | LET el = ELEMENT(doc, selector)
8 |
9 | STYLE_SET(el, "color", "green")
10 | WAIT(200)
11 |
12 | WAIT_STYLE(doc, selector, "color", "rgb(0, 128, 0)")
13 |
14 | LET prev = el.style
15 |
16 | STYLE_SET(el, "color", "red")
17 | WAIT_NO_STYLE(doc, selector, "color", "rgb(0, 128, 0)")
18 | WAIT_STYLE(doc, selector, "color", "rgb(255, 0, 0)")
19 | LET curr = el.style
20 |
21 | T::EQ(prev.color, "rgb(0, 128, 0)")
22 | T::EQ(curr.color, "rgb(255, 0, 0)")
23 |
24 | RETURN NONE
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/attrs/remove_style.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET pageSelector = "#page-events"
4 | LET elemSelector = "#wait-no-style-content"
5 | LET styleName = "color"
6 | LET styleValue = "rgb(100, 100, 100)"
7 |
8 | WAIT_ELEMENT(doc, pageSelector)
9 | LET el = ELEMENT(doc, elemSelector)
10 |
11 | LET prev = el.style.color
12 |
13 | T::NOT::EQ(prev, styleValue)
14 |
15 | STYLE_SET(el, styleName, styleValue)
16 |
17 | LET curr = el.style.color
18 |
19 | T::EQ(curr, styleValue)
20 |
21 | ATTR_REMOVE(el, "style")
22 |
23 | LET removed = el.style.color
24 |
25 | T::EQ(prev, removed)
26 |
27 | RETURN TRUE
--------------------------------------------------------------------------------
/e2e/tests/static/element/value/get.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + '/value.html'
2 | LET doc = DOCUMENT(url)
3 |
4 | LET expected = ["068728","068728","816410","52024413","698690","210583","049700","826394","354369","135911","700285","557242","278832","357701","313034","959368","703500","842750","777175","378061","072489","383005","843393","59912263","464535","229710","230550","767964","758862","944384","025449","010245","844935","038760","013450","124139","211145","758761","448667","488966"]
5 |
6 | LET actual = (
7 | FOR tr IN ELEMENTS(doc, '#listings_table > tbody > tr')
8 | LET elem = ELEMENT(tr, 'td > input')
9 | RETURN elem.value
10 | )
11 |
12 | RETURN T::EQ(actual, expected)
--------------------------------------------------------------------------------
/pkg/drivers/cdp/dom/helpers.go:
--------------------------------------------------------------------------------
1 | package dom
2 |
3 | import (
4 | "bytes"
5 | "regexp"
6 | "strings"
7 | )
8 |
9 | var camelMatcher = regexp.MustCompile("[A-Za-z0-9]+")
10 |
11 | func toCamelCase(input string) string {
12 | var buf bytes.Buffer
13 |
14 | matched := camelMatcher.FindAllString(input, -1)
15 |
16 | if matched == nil {
17 | return ""
18 | }
19 |
20 | for i, match := range matched {
21 | res := match
22 |
23 | if i > 0 {
24 | if len(match) > 1 {
25 | res = strings.ToUpper(match[0:1]) + match[1:]
26 | } else {
27 | res = strings.ToUpper(match)
28 | }
29 | }
30 |
31 | buf.WriteString(res)
32 | }
33 |
34 | return buf.String()
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/dom/loader.go:
--------------------------------------------------------------------------------
1 | package dom
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/mafredri/cdp/protocol/page"
7 | "github.com/mafredri/cdp/protocol/runtime"
8 |
9 | "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
10 | "github.com/MontFerret/ferret/pkg/runtime/core"
11 | )
12 |
13 | type NodeLoader struct {
14 | dom *Manager
15 | }
16 |
17 | func NewNodeLoader(dom *Manager) eval.ValueLoader {
18 | return &NodeLoader{dom}
19 | }
20 |
21 | func (n *NodeLoader) Load(ctx context.Context, frameID page.FrameID, _ eval.RemoteObjectType, _ eval.RemoteClassName, id runtime.RemoteObjectID) (core.Value, error) {
22 | return n.dom.ResolveElement(ctx, frameID, id)
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_int.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_INT checks whether value is a int value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is int, otherwise false.
14 | func IsInt(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.Int), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_none.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_NONE checks whether value is a none value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is none, otherwise false.
14 | func IsNone(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.None), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/runtime/core/getter.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import "context"
4 |
5 | type (
6 | GetterPathIterator interface {
7 | Path() []Value
8 | Current() Value
9 | CurrentIndex() int
10 | }
11 |
12 | // Getter represents an interface of
13 | // complex types that needs to be used to read values by path.
14 | // The interface is created to let user-defined types be used in dot notation data access.
15 | Getter interface {
16 | GetIn(ctx context.Context, path []Value) (Value, PathError)
17 | }
18 |
19 | // GetterFn represents a type of helper functions that implement complex path resolutions.
20 | GetterFn func(ctx context.Context, path []Value, src Getter) (Value, PathError)
21 | )
22 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/acos_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestAcos(t *testing.T) {
14 | Convey("Should return arccosine", t, func() {
15 | out, err := math.Acos(context.Background(), values.NewInt(-1))
16 |
17 | So(err, ShouldBeNil)
18 | So(out.Unwrap(), ShouldEqual, 3.141592653589793)
19 |
20 | out, err = math.Acos(context.Background(), values.NewInt(0))
21 |
22 | So(err, ShouldBeNil)
23 | So(out.Unwrap(), ShouldEqual, 1.5707963267948966)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/int.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // INT asserts that value is a int type.
12 | // @param {Any} actual - Actual value.
13 | // @param {String} [message] - Message to display on error.
14 | var Int = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be int"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.Int, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/doc/wait/no_style_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/events"
2 | LET doc = DOCUMENT(url, true)
3 | LET selector = X("//*[@id='wait-class-btn']")
4 |
5 | WAIT_ELEMENT(doc, "#page-events")
6 |
7 | LET el = ELEMENT(doc, selector)
8 |
9 | STYLE_SET(el, "color", "green")
10 | WAIT(200)
11 |
12 | WAIT_STYLE(doc, selector, "color", "rgb(0, 128, 0)")
13 |
14 | LET prev = el.style
15 |
16 | STYLE_SET(el, "color", "red")
17 | WAIT_NO_STYLE(doc, selector, "color", "rgb(0, 128, 0)")
18 | WAIT_STYLE(doc, selector, "color", "rgb(255, 0, 0)")
19 | LET curr = el.style
20 |
21 | T::EQ(prev.color, "rgb(0, 128, 0)")
22 | T::EQ(curr.color, "rgb(255, 0, 0)")
23 |
24 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/compiler/compiler_eq_test.go:
--------------------------------------------------------------------------------
1 | package compiler_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | . "github.com/smartystreets/goconvey/convey"
8 |
9 | "github.com/MontFerret/ferret/pkg/compiler"
10 | "github.com/MontFerret/ferret/pkg/runtime"
11 | )
12 |
13 | func TestEqualityOperators(t *testing.T) {
14 | Convey("Should compile RETURN 2 > 1", t, func() {
15 | c := compiler.New()
16 |
17 | p, err := c.Compile(`
18 | RETURN 2 > 1
19 | `)
20 |
21 | So(err, ShouldBeNil)
22 | So(p, ShouldHaveSameTypeAs, &runtime.Program{})
23 |
24 | out, err := p.Run(context.Background())
25 |
26 | So(err, ShouldBeNil)
27 | So(string(out), ShouldEqual, "true")
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_array.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_ARRAY checks whether value is an array value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is array, otherwise false.
14 | func IsArray(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.Array), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_float.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_FLOAT checks whether value is a float value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is float, otherwise false.
14 | func IsFloat(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.Float), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/strings/escape_test.go:
--------------------------------------------------------------------------------
1 | package strings_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 |
9 | "github.com/MontFerret/ferret/pkg/stdlib/strings"
10 |
11 | . "github.com/smartystreets/goconvey/convey"
12 | )
13 |
14 | func TestEscapeHTML(t *testing.T) {
15 | Convey("EscapeHTML", t, func() {
16 | Convey("Should escape an HTML string", func() {
17 | out, err := strings.EscapeHTML(context.Background(), values.NewString(`Foobar`))
18 |
19 | So(err, ShouldBeNil)
20 | So(out, ShouldEqual, values.NewString("<body><span>Foobar</span></body>"))
21 | })
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_binary.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_BINARY checks whether value is a binary value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is binary, otherwise false.
14 | func IsBinary(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.Binary), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_boolean.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_BOOL checks whether value is a boolean value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is boolean, otherwise false.
14 | func IsBool(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.Boolean), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_object.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_OBJECT checks whether value is an object value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is object, otherwise false.
14 | func IsObject(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.Object), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_string.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_STRING checks whether value is a string value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is string, otherwise false.
14 | func IsString(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.String), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/degrees_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestDegrees(t *testing.T) {
14 | Convey("Should return a value", t, func() {
15 | out, err := math.Degrees(context.Background(), values.NewFloat(0.7853981633974483))
16 |
17 | So(err, ShouldBeNil)
18 | So(out.Unwrap(), ShouldEqual, 45)
19 |
20 | out, err = math.Degrees(context.Background(), values.NewFloat(3.141592653589793))
21 |
22 | So(err, ShouldBeNil)
23 | So(out.Unwrap(), ShouldEqual, 180)
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/array.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // ARRAY asserts that value is a array type.
12 | // @param {Any} actual - Value to test.
13 | // @param {String} [message] - Message to display on error.
14 | var Array = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be array"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.Array, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/float.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // FLOAT asserts that value is a float type.
12 | // @param {Any} actual - Value to test.
13 | // @param {String} [message] - Message to display on error.
14 | var Float = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be float"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.Float, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/binary.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // BINARY asserts that value is a binary type.
12 | // @param {Any} actual - Value to test.
13 | // @param {String} [message] - Message to display on error.
14 | var Binary = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be binary"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.Binary, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/object.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // OBJECT asserts that value is a object type.
12 | // @param {Any} actual - Value to test.
13 | // @param {String} [message] - Message to display on error.
14 | var Object = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be object"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.Object, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/string.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // STRING asserts that value is a string type.
12 | // @param {Any} actual - Value to test.
13 | // @param {String} [message] - Message to display on error.
14 | var String = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be string"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.String, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/to_binary_test.go:
--------------------------------------------------------------------------------
1 | package types_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | . "github.com/smartystreets/goconvey/convey"
8 |
9 | "github.com/MontFerret/ferret/pkg/runtime/values"
10 | "github.com/MontFerret/ferret/pkg/stdlib/types"
11 | )
12 |
13 | func TestToBinary(t *testing.T) {
14 | Convey("TestToBinary", t, func() {
15 | value := "abc"
16 |
17 | result, err := types.ToBinary(context.Background(), values.NewString(value))
18 | So(err, ShouldBeNil)
19 |
20 | wasBinary, err := types.IsBinary(context.Background(), result)
21 | So(err, ShouldBeNil)
22 | So(wasBinary, ShouldEqual, values.True)
23 |
24 | So(result.String(), ShouldEqual, value)
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/clear/clear_by_css.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET form = ELEMENT(doc, "#page-form")
7 |
8 | INPUT(form, "#text_input", "foo")
9 | INPUT_CLEAR(form, "#text_input")
10 |
11 | LET input = ELEMENT(doc, "#text_input")
12 | LET output = ELEMENT(doc, "#text_output")
13 |
14 | T::EMPTY(output.innerText)
15 |
16 | INPUT(form, "#text_input", "test0-test1", 100)
17 |
18 | INPUT_CLEAR(form, "#text_input")
19 |
20 | T::EMPTY(output.innerText)
21 |
22 | INPUT(form, "#text_input", "test0&test1", 100)
23 |
24 | INPUT_CLEAR(form, "#text_input")
25 |
26 | T::EMPTY(output.innerText)
27 |
28 | RETURN NONE
--------------------------------------------------------------------------------
/pkg/stdlib/math/percentile_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestPercentile(t *testing.T) {
14 | Convey("Should return nth percentile value", t, func() {
15 | out, err := math.Percentile(
16 | context.Background(),
17 | values.NewArrayWith(
18 | values.NewInt(1),
19 | values.NewInt(2),
20 | values.NewInt(3),
21 | values.NewInt(4),
22 | ),
23 | values.NewInt(50),
24 | )
25 |
26 | So(err, ShouldBeNil)
27 | So(out.Unwrap(), ShouldEqual, 2)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/variance_sample_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestSampleVariance(t *testing.T) {
14 | Convey("Should return a value", t, func() {
15 | out, err := math.SampleVariance(
16 | context.Background(),
17 | values.NewArrayWith(
18 | values.NewInt(1),
19 | values.NewInt(3),
20 | values.NewInt(6),
21 | values.NewInt(5),
22 | values.NewInt(2),
23 | ),
24 | )
25 |
26 | So(err, ShouldBeNil)
27 | So(out.Unwrap(), ShouldEqual, 4.3)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_date_time.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_DATETIME checks whether value is a date time value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is date time, otherwise false.
14 | func IsDateTime(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], types.DateTime), nil
22 | }
23 |
--------------------------------------------------------------------------------
/examples/while.fql:
--------------------------------------------------------------------------------
1 | LET doc = DOCUMENT("https://github.com/MontFerret/ferret/stargazers", { driver: "cdp" })
2 |
3 | LET nextSelector = '[data-test-selector="pagination"] .btn:nth-child(2):not([disabled])'
4 | LET elementsSelector = '#repos ol li'
5 |
6 | FOR i DO WHILE ELEMENT_EXISTS(doc, nextSelector)
7 | LIMIT 3
8 | LET wait = i > 0 ? CLICK(doc, nextSelector) : false
9 | LET nav = wait ? (WAITFOR EVENT "navigation" IN doc) : false
10 |
11 | FOR el IN ELEMENTS(doc, elementsSelector)
12 | FILTER ELEMENT_EXISTS(el, ".octicon-organization")
13 |
14 | RETURN {
15 | name: INNER_TEXT(el, 'div > div:nth-child(2) [data-hovercard-type="user"]'),
16 | company: INNER_TEXT(el, "div > div:nth-child(2) p")
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/datetime.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // DATETIME asserts that value is a datetime type.
12 | // @param {Any} actual - Value to test.
13 | // @param {String} [message] - Message to display on error.
14 | var DateTime = base.Assertion{
15 | DefaultMessage: func(_ []core.Value) string {
16 | return "be datetime"
17 | },
18 | MinArgs: 1,
19 | MaxArgs: 2,
20 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
21 | return args[0].Type() == types.DateTime, nil
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_html_element.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers"
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | )
10 |
11 | // IS_HTML_ELEMENT checks whether value is a HTMLElement value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is HTMLElement, otherwise false.
14 | func IsHTMLElement(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], drivers.HTMLElementType), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/variance_population_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestPopulationVariance(t *testing.T) {
14 | Convey("Should return a value", t, func() {
15 | out, err := math.PopulationVariance(
16 | context.Background(),
17 | values.NewArrayWith(
18 | values.NewInt(1),
19 | values.NewInt(3),
20 | values.NewInt(6),
21 | values.NewInt(5),
22 | values.NewInt(2),
23 | ),
24 | )
25 |
26 | So(err, ShouldBeNil)
27 | So(out.Unwrap(), ShouldEqual, 3.44)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/strings/unescape_test.go:
--------------------------------------------------------------------------------
1 | package strings_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 |
9 | "github.com/MontFerret/ferret/pkg/stdlib/strings"
10 |
11 | . "github.com/smartystreets/goconvey/convey"
12 | )
13 |
14 | func TestUnescapeHTML(t *testing.T) {
15 | Convey("UnescapeHTML", t, func() {
16 | Convey("Should unescape an string", func() {
17 | out, err := strings.UnescapeHTML(context.Background(), values.NewString("<body><span>Foobar</span></body>"))
18 |
19 | expected := values.NewString("Foobar")
20 | So(err, ShouldBeNil)
21 | So(out, ShouldEqual, expected)
22 | })
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_html_document.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers"
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | )
10 |
11 | // IS_HTML_DOCUMENT checks whether value is a HTMLDocument value.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is HTMLDocument, otherwise false.
14 | func IsHTMLDocument(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return isTypeof(args[0], drivers.HTMLDocumentType), nil
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/runtime/collections/collection.go:
--------------------------------------------------------------------------------
1 | package collections
2 |
3 | import (
4 | "github.com/MontFerret/ferret/pkg/runtime/core"
5 | "github.com/MontFerret/ferret/pkg/runtime/values"
6 | )
7 |
8 | type (
9 | // Measurable represents an interface of a value that can has length.
10 | Measurable interface {
11 | Length() values.Int
12 | }
13 |
14 | IndexedCollection interface {
15 | core.Value
16 | Measurable
17 | Get(idx values.Int) core.Value
18 | Set(idx values.Int, value core.Value) error
19 | }
20 |
21 | KeyedCollection interface {
22 | core.Value
23 | Measurable
24 | Keys() []values.String
25 | Get(key values.String) (core.Value, values.Boolean)
26 | Set(key values.String, value core.Value)
27 | }
28 | )
29 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/none.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
10 | )
11 |
12 | // NONE asserts that value is none.
13 | // @param {Any} actual - Value to test.
14 | // @param {String} [message] - Message to display on error.
15 | var None = base.Assertion{
16 | DefaultMessage: func(_ []core.Value) string {
17 | return fmt.Sprintf("be %s", base.FormatValue(values.None))
18 | },
19 | MinArgs: 1,
20 | MaxArgs: 2,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return args[0] == values.None, nil
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/true.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
10 | )
11 |
12 | // TRUE asserts that value is true.
13 | // @param {Any} actual - Value to test.
14 | // @param {String} [message] - Message to display on error.
15 | var True = base.Assertion{
16 | DefaultMessage: func(_ []core.Value) string {
17 | return fmt.Sprintf("be %s", base.FormatValue(values.True))
18 | },
19 | MinArgs: 1,
20 | MaxArgs: 2,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return args[0] == values.True, nil
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/false.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
10 | )
11 |
12 | // FALSE asserts that value is false.
13 | // @param {Any}actual - Value to test.
14 | // @param {String} [message] - Message to display on error.
15 | var False = base.Assertion{
16 | DefaultMessage: func(_ []core.Value) string {
17 | return fmt.Sprintf("be %s", base.FormatValue(values.False))
18 | },
19 | MinArgs: 1,
20 | MaxArgs: 2,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return args[0] == values.False, nil
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/stddev_sample_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestStandardDeviationSample(t *testing.T) {
14 | Convey("Should return a value", t, func() {
15 | out, err := math.StandardDeviationSample(
16 | context.Background(),
17 | values.NewArrayWith(
18 | values.NewInt(1),
19 | values.NewInt(3),
20 | values.NewInt(6),
21 | values.NewInt(5),
22 | values.NewInt(2),
23 | ),
24 | )
25 |
26 | So(err, ShouldBeNil)
27 | So(out.Unwrap(), ShouldEqual, 2.073644135332772)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/html/elements.go:
--------------------------------------------------------------------------------
1 | package html
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // ELEMENTS finds HTML elements by a given CSS selector.
11 | // Returns an empty array if element not found.
12 | // @param {HTMLPage | HTMLDocument | HTMLElement} node - Target html node.
13 | // @param {String} selector - CSS selector.
14 | // @return {HTMLElement[]} - An array of matched HTML elements.
15 | func Elements(ctx context.Context, args ...core.Value) (core.Value, error) {
16 | el, selector, err := queryArgs(args)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | return el.QuerySelectorAll(ctx, selector)
23 | }
24 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/element/clear/clear_by_xpath.fql:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.dynamic + "?redirect=/forms"
2 | LET doc = DOCUMENT(url, true)
3 |
4 | WAIT_ELEMENT(doc, "form")
5 |
6 | LET form = ELEMENT(doc, "#page-form")
7 |
8 | INPUT(form, "#text_input", "foo")
9 | INPUT_CLEAR(form, X('//*[@id="text_input"]'))
10 |
11 | LET input = ELEMENT(doc, "#text_input")
12 | LET output = ELEMENT(doc, "#text_output")
13 |
14 | T::EMPTY(output.innerText)
15 |
16 | INPUT(form, "#text_input", "test0-test1", 100)
17 |
18 | INPUT_CLEAR(form, X('//*[@id="text_input"]'))
19 |
20 | T::EMPTY(output.innerText)
21 |
22 | INPUT(form, "#text_input", "test0&test1", 100)
23 |
24 | INPUT_CLEAR(form, X('//*[@id="text_input"]'))
25 |
26 | T::EMPTY(output.innerText)
27 |
28 | RETURN NONE
--------------------------------------------------------------------------------
/revive.toml:
--------------------------------------------------------------------------------
1 | ignoreGeneratedHeader = true
2 | severity = "error"
3 | confidence = 0.8
4 | errorCode = 1
5 | warningCode = 0
6 |
7 | [rule.blank-imports]
8 | [rule.context-as-argument]
9 | [rule.context-keys-type]
10 | [rule.dot-imports]
11 | Disabled = true
12 | [rule.error-return]
13 | [rule.error-strings]
14 | [rule.error-naming]
15 | [rule.if-return]
16 | [rule.increment-decrement]
17 | [rule.var-naming]
18 | [rule.var-declaration]
19 | [rule.range]
20 | [rule.receiver-naming]
21 | [rule.time-naming]
22 | [rule.unexported-return]
23 | [rule.indent-error-flow]
24 | [rule.errorf]
25 | [rule.empty-block]
26 | [rule.superfluous-else]
27 | [rule.unused-parameter]
28 | severity = "warning"
29 | [rule.unreachable-code]
30 | [rule.redefines-builtin-id]
31 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/stddev_population_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestStandardDeviationPopulation(t *testing.T) {
14 | Convey("Should return a value", t, func() {
15 | out, err := math.StandardDeviationPopulation(
16 | context.Background(),
17 | values.NewArrayWith(
18 | values.NewInt(1),
19 | values.NewInt(3),
20 | values.NewInt(6),
21 | values.NewInt(5),
22 | values.NewInt(2),
23 | ),
24 | )
25 |
26 | So(err, ShouldBeNil)
27 | So(out.Unwrap(), ShouldEqual, 1.8547236990991407)
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/equal.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // EQUAL asserts equality of actual and expected values.
12 | // @param {Any} actual - Actual value.
13 | // @param {Any} expected - Expected value.
14 | // @param {String} [message] - Message to display on error.
15 | var Equal = base.Assertion{
16 | DefaultMessage: func(args []core.Value) string {
17 | return fmt.Sprintf("be %s %s", base.EqualOp, base.FormatValue(args[1]))
18 | },
19 | MinArgs: 2,
20 | MaxArgs: 3,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return base.EqualOp.Compare(args)
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/lt.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // LT asserts that an actual value is lesser than an expected one.
12 | // @param {Any} actual - Actual value.
13 | // @param {Any} expected - Expected value.
14 | // @param {String} [message] - Message to display on error.
15 | var Lt = base.Assertion{
16 | DefaultMessage: func(args []core.Value) string {
17 | return fmt.Sprintf("be %s %s", base.LessOp, base.FormatValue(args[1]))
18 | },
19 | MinArgs: 2,
20 | MaxArgs: 3,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return base.LessOp.Compare(args)
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/e2e/tests/dynamic/page/cookies/override_default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "cdp_cookies",
4 | cookies: [
5 | {
6 | name: "Single_cookie",
7 | value: "Foo"
8 | },
9 | {
10 | name: "Multi_set_cookie",
11 | value: "Bar"
12 | }
13 | ]
14 | })
15 |
16 | LET el = ELEMENT(page, "#cookies")
17 | LET actual = (
18 | FOR c IN JSON_PARSE(el.innerText)
19 | SORT c.Name
20 | RETURN c
21 | )
22 |
23 | LET expected = {
24 | "Single_cookie": "Foo",
25 | "Multi_set_cookie": "Bar",
26 | }
27 |
28 | RETURN T::EQ({
29 | "Single_cookie": actual[1].Value,
30 | "Multi_set_cookie": actual[0].Value,
31 | }, expected)
--------------------------------------------------------------------------------
/e2e/tests/static/doc/cookies/override_default.fqlx:
--------------------------------------------------------------------------------
1 | LET url = @lab.cdn.static + "/api/ts"
2 | LET page = DOCUMENT(url, {
3 | driver: "http_cookies",
4 | cookies: [
5 | {
6 | name: "Single_cookie",
7 | value: "Foo"
8 | },
9 | {
10 | name: "Multi_set_cookie",
11 | value: "Bar"
12 | }
13 | ]
14 | })
15 |
16 | LET el = ELEMENT(page, "#cookies")
17 | LET actual = (
18 | FOR c IN JSON_PARSE(el.innerText)
19 | SORT c.Name
20 | RETURN c
21 | )
22 |
23 | LET expected = {
24 | "Single_cookie": "Foo",
25 | "Multi_set_cookie": "Bar",
26 | }
27 |
28 | RETURN T::EQ({
29 | "Single_cookie": actual[1].Value,
30 | "Multi_set_cookie": actual[0].Value,
31 | }, expected)
--------------------------------------------------------------------------------
/pkg/drivers/common/types.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "golang.org/x/net/html"
5 | )
6 |
7 | func FromHTMLType(nt html.NodeType) int {
8 | switch nt {
9 | case html.DocumentNode:
10 | return 9
11 | case html.ElementNode:
12 | return 1
13 | case html.TextNode:
14 | return 3
15 | case html.CommentNode:
16 | return 8
17 | case html.DoctypeNode:
18 | return 10
19 | }
20 |
21 | return 0
22 | }
23 |
24 | func ToHTMLType(input int) html.NodeType {
25 | switch input {
26 | case 1:
27 | return html.ElementNode
28 | case 3:
29 | return html.TextNode
30 | case 8:
31 | return html.CommentNode
32 | case 9:
33 | return html.DocumentNode
34 | case 10:
35 | return html.DoctypeNode
36 | default:
37 | return html.ErrorNode
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/gt.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // GT asserts that an actual value is greater than an expected one.
12 | // @param {Any} actual - Actual value.
13 | // @param {Any} expected - Expected value.
14 | // @param {String} [message] - Message to display on error.
15 | var Gt = base.Assertion{
16 | DefaultMessage: func(args []core.Value) string {
17 | return fmt.Sprintf("be %s %s", base.GreaterOp, base.FormatValue(args[1]))
18 | },
19 | MinArgs: 2,
20 | MaxArgs: 3,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return base.GreaterOp.Compare(args)
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/types/is_nan.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // IS_NAN checks whether value is NaN.
12 | // @param {Any} value - Input value of arbitrary type.
13 | // @return {Boolean} - Returns true if value is NaN, otherwise false.
14 | func IsNaN(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | if args[0].Type() != types.Float {
22 | return values.False, nil
23 | }
24 |
25 | return values.IsNaN(args[0].(values.Float)), nil
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/drivers/cdp/templates/helpers.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers"
7 | "github.com/MontFerret/ferret/pkg/drivers/cdp/eval"
8 | )
9 |
10 | var (
11 | notFoundErrorFragment = fmt.Sprintf(`
12 | if (found == null) {
13 | throw new Error(%s);
14 | }
15 | `, ParamErr(drivers.ErrNotFound))
16 | )
17 |
18 | func ParamErr(err error) string {
19 | return EscapeString(err.Error())
20 | }
21 |
22 | func EscapeString(value string) string {
23 | return "`" + value + "`"
24 | }
25 |
26 | func toFunction(selector drivers.QuerySelector, cssTmpl, xPathTmpl string) *eval.Function {
27 | if selector.Kind() == drivers.CSSSelector {
28 | return eval.F(cssTmpl)
29 | }
30 |
31 | return eval.F(xPathTmpl)
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/runtime/values/types/helpers.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import "github.com/MontFerret/ferret/pkg/runtime/core"
4 |
5 | // Comparison table of builtin types
6 | var typeComparisonTable = map[core.Type]uint64{
7 | None: 0,
8 | Boolean: 1,
9 | Int: 2,
10 | Float: 3,
11 | String: 4,
12 | DateTime: 5,
13 | Array: 6,
14 | Object: 7,
15 | Binary: 8,
16 | }
17 |
18 | func Compare(first, second core.Type) int64 {
19 | f, ok := typeComparisonTable[first]
20 |
21 | // custom type
22 | if !ok {
23 | return -1
24 | }
25 |
26 | s, ok := typeComparisonTable[second]
27 |
28 | // custom type
29 | if !ok {
30 | return 1
31 | }
32 |
33 | if f == s {
34 | return 0
35 | }
36 |
37 | if f > s {
38 | return 1
39 | }
40 |
41 | return -1
42 | }
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Browser [e.g. chrome, safari]
26 | - Version [e.g. 22]
27 |
28 | **Additional context**
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/html/element_exists.go:
--------------------------------------------------------------------------------
1 | package html
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // ELEMENT_EXISTS returns a boolean value indicating whether there is an element matched by selector.
11 | // @param {HTMLPage | HTMLDocument | HTMLElement} node - Target html node.
12 | // @param {String} selector - CSS selector.
13 | // @return {Boolean} - A boolean value indicating whether there is an element matched by selector.
14 | func ElementExists(ctx context.Context, args ...core.Value) (core.Value, error) {
15 | el, selector, err := queryArgs(args)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | return el.ExistsBySelector(ctx, selector)
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/tan.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "context"
5 | "math"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
10 | )
11 |
12 | // TAN returns the tangent of a given number.
13 | // @param {Int | Float} number - A number.
14 | // @return {Float} - The tangent.
15 | func Tan(_ context.Context, args ...core.Value) (core.Value, error) {
16 | err := core.ValidateArgs(args, 1, 1)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | err = core.ValidateType(args[0], types.Int, types.Float)
23 |
24 | if err != nil {
25 | return values.None, err
26 | }
27 |
28 | return values.NewFloat(math.Tan(toFloat(args[0]))), nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/lte.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // LTE asserts that an actual value is lesser than or equal to an expected one.
12 | // @param {Any} actual - Actual value.
13 | // @param {Any} expected - Expected value.
14 | // @param {String} [message] - Message to display on error.
15 | var Lte = base.Assertion{
16 | DefaultMessage: func(args []core.Value) string {
17 | return fmt.Sprintf("be %s %s", base.LessOrEqualOp, base.FormatValue(args[1]))
18 | },
19 | MinArgs: 2,
20 | MaxArgs: 3,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return base.LessOrEqualOp.Compare(args)
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/arrays/first.go:
--------------------------------------------------------------------------------
1 | package arrays
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // FIRST returns a first element from a given array.
12 | // @param {Any[]} arr - Target array.
13 | // @return {Any} - First element in a given array.
14 | func First(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | err = core.ValidateType(args[0], types.Array)
22 |
23 | if err != nil {
24 | return values.None, nil
25 | }
26 |
27 | arr := args[0].(*values.Array)
28 |
29 | return arr.Get(0), nil
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/day.go:
--------------------------------------------------------------------------------
1 | package datetime
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // DATE_DAY returns the day of date as a number.
12 | // @param {DateTime} date - Source DateTime.
13 | // @return {Int} - A day number.
14 | func DateDay(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | err = core.ValidateType(args[0], types.DateTime)
21 | if err != nil {
22 | return values.None, err
23 | }
24 |
25 | day := args[0].(values.DateTime).Day()
26 |
27 | return values.NewInt(day), nil
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/stdlib/html/elements_count.go:
--------------------------------------------------------------------------------
1 | package html
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | )
9 |
10 | // ELEMENTS_COUNT returns a number of found HTML elements by a given CSS selector.
11 | // Returns an empty array if element not found.
12 | // @param {HTMLPage | HTMLDocument | HTMLElement} node - Target html node.
13 | // @param {String} selector - CSS selector.
14 | // @return {Int} - A number of matched HTML elements by a given CSS selector.
15 | func ElementsCount(ctx context.Context, args ...core.Value) (core.Value, error) {
16 | el, selector, err := queryArgs(args)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | return el.CountBySelector(ctx, selector)
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/sqrt.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "context"
5 | "math"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
10 | )
11 |
12 | // SQRT returns the square root of a given number.
13 | // @param {Int | Float} value - A number.
14 | // @return {Float} - The square root.
15 | func Sqrt(_ context.Context, args ...core.Value) (core.Value, error) {
16 | err := core.ValidateArgs(args, 1, 1)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | err = core.ValidateType(args[0], types.Int, types.Float)
23 |
24 | if err != nil {
25 | return values.None, err
26 | }
27 |
28 | return values.NewFloat(math.Sqrt(toFloat(args[0]))), nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/fail_test.go:
--------------------------------------------------------------------------------
1 | package testing_test
2 |
3 | import (
4 | "context"
5 | t "testing"
6 |
7 | . "github.com/smartystreets/goconvey/convey"
8 |
9 | "github.com/MontFerret/ferret/pkg/runtime/values"
10 | "github.com/MontFerret/ferret/pkg/stdlib/testing"
11 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
12 | )
13 |
14 | func TestFail(t *t.T) {
15 | Fail := base.NewPositiveAssertion(testing.Fail)
16 |
17 | Convey("When arg is not passed", t, func() {
18 | Convey("It should return an error", func() {
19 | _, err := Fail(context.Background())
20 |
21 | So(err, ShouldBeError)
22 | })
23 | })
24 |
25 | Convey("It should return an error", t, func() {
26 | _, err := Fail(context.Background(), values.False)
27 |
28 | So(err, ShouldBeError)
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/stdlib/testing/gte.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/stdlib/testing/base"
9 | )
10 |
11 | // GTE asserts that an actual value is greater than or equal to an expected one.
12 | // @param {Any} actual - Actual value.
13 | // @param {Any} expected - Expected value.
14 | // @param {String} [message] - Message to display on error.
15 | var Gte = base.Assertion{
16 | DefaultMessage: func(args []core.Value) string {
17 | return fmt.Sprintf("be %s %s", base.GreaterOrEqualOp, base.FormatValue(args[1]))
18 | },
19 | MinArgs: 2,
20 | MaxArgs: 3,
21 | Fn: func(_ context.Context, args []core.Value) (bool, error) {
22 | return base.GreaterOrEqualOp.Compare(args)
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/hour.go:
--------------------------------------------------------------------------------
1 | package datetime
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // DATE_HOUR returns the hour of date as a number.
12 | // @param {DateTime} date - Source DateTime.
13 | // @return {Int} - An hour number.
14 | func DateHour(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | err = core.ValidateType(args[0], types.DateTime)
21 | if err != nil {
22 | return values.None, err
23 | }
24 |
25 | hour := args[0].(values.DateTime).Hour()
26 |
27 | return values.NewInt(hour), nil
28 | }
29 |
--------------------------------------------------------------------------------
/examples/google-search.fql:
--------------------------------------------------------------------------------
1 | LET google = DOCUMENT("https://www.google.com/", {
2 | driver: "cdp",
3 | userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36"
4 | })
5 |
6 | HOVER(google, 'input[name="q"]')
7 | WAIT(RAND(100))
8 | INPUT(google, 'input[name="q"]', @criteria, 30)
9 | WAIT(RAND(100))
10 | CLICK(google, 'input[name="btnK"]')
11 |
12 | WAITFOR EVENT "navigation" IN google
13 |
14 | WAIT_ELEMENT(google, "#res")
15 |
16 | LET results = ELEMENTS(google, X("//*[text() = 'Search Results']/following-sibling::*/*"))
17 |
18 | FOR el IN results
19 | RETURN {
20 | title: INNER_TEXT(el, 'h3')?,
21 | description: INNER_TEXT(el, X("//em/parent::*")),
22 | url: ELEMENT(el, 'a')?.attributes.href
23 | }
--------------------------------------------------------------------------------
/pkg/stdlib/arrays/last.go:
--------------------------------------------------------------------------------
1 | package arrays
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // LAST returns the last element of an array.
12 | // @param {Any[]} array - The target array.
13 | // @return {Any} - Last element of an array.
14 | func Last(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 |
17 | if err != nil {
18 | return values.None, err
19 | }
20 |
21 | err = core.ValidateType(args[0], types.Array)
22 |
23 | if err != nil {
24 | return values.None, nil
25 | }
26 |
27 | arr := args[0].(*values.Array)
28 |
29 | return arr.Get(arr.Length() - 1), nil
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/cos.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "context"
5 | "math"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
10 | )
11 |
12 | // COS returns the cosine of a given number.
13 | // @param {Int | Float} number - Input number.
14 | // @return {Float} - The cosine of a given number.
15 | func Cos(_ context.Context, args ...core.Value) (core.Value, error) {
16 | err := core.ValidateArgs(args, 1, 1)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | err = core.ValidateType(args[0], types.Int, types.Float)
23 |
24 | if err != nil {
25 | return values.None, err
26 | }
27 |
28 | return values.NewFloat(math.Cos(toFloat(args[0]))), nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/strings/escape.go:
--------------------------------------------------------------------------------
1 | package strings
2 |
3 | import (
4 | "context"
5 | "html"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | )
10 |
11 | // ESCAPE_HTML escapes special characters like "<" to become "<". It
12 | // escapes only five such characters: <, >, &, ' and ".
13 | // UnescapeString(EscapeString(s)) == s always holds, but the converse isn't
14 | // always true.
15 | // @param {String} uri - Uri to escape.
16 | // @return {String} - Escaped string.
17 | func EscapeHTML(_ context.Context, args ...core.Value) (core.Value, error) {
18 | err := core.ValidateArgs(args, 1, 1)
19 |
20 | if err != nil {
21 | return values.None, err
22 | }
23 |
24 | return values.NewString(html.EscapeString(args[0].String())), nil
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/second.go:
--------------------------------------------------------------------------------
1 | package datetime
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // DATE_SECOND returns the second of date as a number.
12 | // @param {DateTime} date - Source DateTime.
13 | // @return {Int} - A second number.
14 | func DateSecond(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | err = core.ValidateType(args[0], types.DateTime)
21 | if err != nil {
22 | return values.None, err
23 | }
24 |
25 | sec := args[0].(values.DateTime).Second()
26 |
27 | return values.NewInt(sec), nil
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/year.go:
--------------------------------------------------------------------------------
1 | package datetime
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // DATE_YEAR returns the year extracted from the given date.
12 | // @param {DateTime} date - Source DateTime.
13 | // @return {Int} - A year number.
14 | func DateYear(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | err = core.ValidateType(args[0], types.DateTime)
21 | if err != nil {
22 | return values.None, err
23 | }
24 |
25 | year := args[0].(values.DateTime).Year()
26 |
27 | return values.NewInt(year), nil
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/exp2.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "context"
5 | "math"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
10 | )
11 |
12 | // EXP2 returns 2 raised to the power of value.
13 | // @param {Int | Float} number - Input number.
14 | // @return {Float} - 2 raised to the power of value.
15 | func Exp2(_ context.Context, args ...core.Value) (core.Value, error) {
16 | err := core.ValidateArgs(args, 1, 1)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | err = core.ValidateType(args[0], types.Int, types.Float)
23 |
24 | if err != nil {
25 | return values.None, err
26 | }
27 |
28 | return values.NewFloat(math.Exp2(toFloat(args[0]))), nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/stdlib/datetime/month.go:
--------------------------------------------------------------------------------
1 | package datetime
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | // DATE_MONTH returns the month of date as a number.
12 | // @param {DateTime} date - Source DateTime.
13 | // @return {Int} - A month number.
14 | func DateMonth(_ context.Context, args ...core.Value) (core.Value, error) {
15 | err := core.ValidateArgs(args, 1, 1)
16 | if err != nil {
17 | return values.None, err
18 | }
19 |
20 | err = core.ValidateType(args[0], types.DateTime)
21 | if err != nil {
22 | return values.None, err
23 | }
24 |
25 | month := args[0].(values.DateTime).Month()
26 |
27 | return values.NewInt(int(month)), nil
28 | }
29 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/ceil_test.go:
--------------------------------------------------------------------------------
1 | package math_test
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/stdlib/math"
9 |
10 | . "github.com/smartystreets/goconvey/convey"
11 | )
12 |
13 | func TestCeil(t *testing.T) {
14 | Convey("Should return a value", t, func() {
15 | out, err := math.Ceil(context.Background(), values.NewFloat(2.49))
16 |
17 | So(err, ShouldBeNil)
18 | So(out.Unwrap(), ShouldEqual, 3)
19 |
20 | out, err = math.Ceil(context.Background(), values.NewFloat(2.50))
21 |
22 | So(err, ShouldBeNil)
23 | So(out.Unwrap(), ShouldEqual, 3)
24 |
25 | out, err = math.Ceil(context.Background(), values.NewFloat(-2.50))
26 |
27 | So(err, ShouldBeNil)
28 | So(out.Unwrap(), ShouldEqual, -2)
29 | })
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/mean.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "math"
5 |
6 | "github.com/MontFerret/ferret/pkg/runtime/core"
7 | "github.com/MontFerret/ferret/pkg/runtime/values"
8 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
9 | )
10 |
11 | func mean(input *values.Array) (values.Float, error) {
12 | if input.Length() == 0 {
13 | return values.NewFloat(math.NaN()), nil
14 | }
15 |
16 | var err error
17 | var sum float64
18 |
19 | input.ForEach(func(value core.Value, _ int) bool {
20 | err = core.ValidateType(value, types.Int, types.Float)
21 |
22 | if err != nil {
23 | return false
24 | }
25 |
26 | sum += toFloat(value)
27 |
28 | return true
29 | })
30 |
31 | if err != nil {
32 | return 0, err
33 | }
34 |
35 | return values.NewFloat(sum / float64(input.Length())), nil
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/stdlib/math/sin.go:
--------------------------------------------------------------------------------
1 | package math
2 |
3 | import (
4 | "context"
5 | "math"
6 |
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | "github.com/MontFerret/ferret/pkg/runtime/values/types"
10 | )
11 |
12 | // SIN returns the sine of the radian argument.
13 | // @param {Int | Float} number - Input number.
14 | // @return {Float} - The sin, in radians, of a given number.
15 | func Sin(_ context.Context, args ...core.Value) (core.Value, error) {
16 | err := core.ValidateArgs(args, 1, 1)
17 |
18 | if err != nil {
19 | return values.None, err
20 | }
21 |
22 | err = core.ValidateType(args[0], types.Int, types.Float)
23 |
24 | if err != nil {
25 | return values.None, err
26 | }
27 |
28 | return values.NewFloat(math.Sin(toFloat(args[0]))), nil
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/drivers/common/frames.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/MontFerret/ferret/pkg/drivers"
7 | "github.com/MontFerret/ferret/pkg/runtime/core"
8 | "github.com/MontFerret/ferret/pkg/runtime/values"
9 | )
10 |
11 | func CollectFrames(ctx context.Context, receiver *values.Array, doc drivers.HTMLDocument) error {
12 | receiver.Push(doc)
13 |
14 | children, err := doc.GetChildDocuments(ctx)
15 |
16 | if err != nil {
17 | return err
18 | }
19 |
20 | children.ForEach(func(value core.Value, _ int) bool {
21 | childDoc, ok := value.(drivers.HTMLDocument)
22 |
23 | if !ok {
24 | err = core.TypeError(value.Type(), drivers.HTMLDocumentType)
25 |
26 | return false
27 | }
28 |
29 | return CollectFrames(ctx, receiver, childDoc) == nil
30 | })
31 |
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------