├── 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 = '
  • Containers
  • Responsive breakpoints
  • Z-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 = '
  • Containers
  • Responsive breakpoints
  • Z-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 | --------------------------------------------------------------------------------