From 90427cd537f2f28cac06c27d0e09d3b9c47eafb0 Mon Sep 17 00:00:00 2001 From: Tim Voronov Date: Thu, 16 Sep 2021 21:40:20 -0400 Subject: [PATCH] Feature/new selector type (#657) * Added remote type reference resolver * Added support of XPath query selector * Added CDP e2e testss covering XPath integration * Added additional CDP e2e tests covering XPath integration * Added type check to QuerySelector casting function * Fixed XPath e2e tests * Fixed vuln issue * Added support of XPath selectors to http driver * Added e2e tests for XPAth --- ...click_by_selector.fql => click_by_css.fql} | 2 +- .../click/click_by_css_with_count.fql} | 2 +- .../dynamic/doc/click/click_by_xpath.fql | 8 + .../doc/click/click_by_xpath_with_count.fql | 16 + .../element/{exists.fql => exists_by_css.fql} | 2 +- .../dynamic/doc/element/exists_by_xpath.fql | 7 + e2e/tests/dynamic/doc/focus/focus_by_css.fql | 8 + .../dynamic/doc/focus/focus_by_xpath.fql | 8 + .../{get_by_selector.fql => get_by_css.fql} | 0 ...by_selector_all.fql => get_by_css_all.fql} | 0 .../dynamic/doc/inner_html/get_by_xpath.fql | 10 + .../doc/inner_html/get_by_xpath_all.fql | 13 + .../{get_by_selector.fql => get_by_css.fql} | 0 ...by_selector_all.fql => get_by_css_all.fql} | 0 .../dynamic/doc/inner_text/get_by_xpath.fql | 10 + .../doc/inner_text/get_by_xpath_all.fql | 16 + .../doc/input/{input.fql => input_by_css.fql} | 0 .../dynamic/doc/input/input_by_xpath.fql | 10 + .../{pagination.fql => pagination_by_css.fql} | 0 e2e/tests/dynamic/doc/pagination_by_xpath.fql | 9 + .../select/{multi.fql => multi_by_css.fql} | 0 .../dynamic/doc/select/multi_by_xpath.fql | 12 + .../select/{single.fql => single_by_css.fql} | 0 .../dynamic/doc/select/single_by_xpath.fql | 12 + .../{attr_all.fql => attr_all_by_css.fql} | 0 .../dynamic/doc/wait/attr_all_by_xpath.fql | 10 + .../doc/wait/{attr.fql => attr_by_css.fql} | 0 e2e/tests/dynamic/doc/wait/attr_by_xpath.fql | 20 ++ .../{class_all.fql => class_all_by_css.fql} | 0 .../dynamic/doc/wait/class_all_by_xpath.fql | 9 + .../doc/wait/{class.fql => class_by_css.fql} | 0 e2e/tests/dynamic/doc/wait/class_by_xpath.fql | 14 + .../wait/{element.fql => element_by_css.fql} | 0 .../dynamic/doc/wait/element_by_xpath.fql | 15 + .../dynamic/doc/wait/frame_navigation.fqlx | 5 - .../wait/{no_attr.fql => no_attr_by_css.fql} | 0 .../dynamic/doc/wait/no_attr_by_xpath.fql | 16 + ..._class_all.fql => no_class_all_by_css.fql} | 0 .../doc/wait/no_class_all_by_xpath.fql | 9 + .../{no_class.fql => no_class_by_css.fql} | 0 .../dynamic/doc/wait/no_class_by_xpath.fql | 14 + .../{no_element.fql => no_element_by_css.fql} | 0 .../dynamic/doc/wait/no_element_by_xpath.fql | 15 + ..._style_all.fql => no_style_all_by_css.fql} | 0 .../doc/wait/no_style_all_by_xpath.fql | 32 ++ .../{no_style.fql => no_style_by_css.fql} | 0 .../dynamic/doc/wait/no_style_by_xpath.fql | 24 ++ .../{style_all.fql => style_all_by_css.fql} | 0 .../dynamic/doc/wait/style_all_by_xpath.fql | 23 ++ .../doc/wait/{style.fql => style_by_css.fql} | 0 e2e/tests/dynamic/doc/wait/style_by_xpath.fql | 18 + .../{blur_by_selector.fql => blur_by_css.fql} | 2 +- .../dynamic/element/blur/blur_by_xpath.fql | 12 + ...clear_by_selector.fql => clear_by_css.fql} | 0 .../dynamic/element/clear/clear_by_xpath.fql | 28 ++ ...click_by_selector.fql => click_by_css.fql} | 2 +- .../click/click_by_css_with_count.fql} | 0 .../dynamic/element/click/click_by_xpath.fql | 10 + .../click/click_by_xpath_with_count.fql | 16 + ...focus_by_selector.fql => focus_by_css.fql} | 2 +- .../focus/focus_by_xpath.fql} | 4 +- ...hover_by_selector.fql => hover_by_css.fql} | 0 .../dynamic/element/hover/hover_by_xpath.fql | 11 + .../{get_by_selector.fql => get_by_css.fql} | 0 ...fail.fql => get_by_css_not_found.fail.fql} | 0 .../element/inner_html/get_by_xpath.fql | 34 ++ .../get_by_xpath_not_found.fail.fql | 5 + .../{set_by_selector.fql => set_by_css.fql} | 0 .../element/inner_html/set_by_xpath.fql | 25 ++ .../{get_by_selector.fql => get_by_css.fql} | 0 ...fail.fql => get_by_css_not_found.fail.fql} | 0 .../element/inner_text/get_by_xpath.fql | 35 ++ .../get_by_xpath_not_found.fail.fql | 5 + ..._selector.fail.fql => set_by_css.fail.fql} | 0 .../{set_by_selector.fql => set_by_css.fql} | 0 .../element/inner_text/set_by_xpath.fail.fql | 5 + .../element/inner_text/set_by_xpath.fql | 37 ++ ...input_by_selector.fql => input_by_css.fql} | 0 ...eout.fql => input_by_css_with_timeout.fql} | 0 .../dynamic/element/input/input_by_xpath.fql | 12 + .../input/input_by_xpath_with_timeout.fql | 12 + .../{press_selector.fql => press_by_css.fql} | 0 .../dynamic/element/press/press_by_xpath.fql | 10 + .../dynamic/element/query/element_by_css.fql | 8 + .../element/query/element_by_xpath.fql | 8 + .../dynamic/element/query/elements_by_css.fql | 8 + .../element/query/elements_by_xpath.fql | 8 + .../dynamic/element/query/exists_by_xpath.fql | 9 + .../element/{exists.fql => exists_by_css.fql} | 0 .../static/doc/element/exists_by_xpath.fql | 7 + .../{get_by_selector.fql => get_by_css.fql} | 0 ...by_selector_all.fql => get_by_css_all.fql} | 0 .../static/doc/inner_html/get_by_xpath.fql | 7 + .../doc/inner_html/get_by_xpath_all.fql | 14 + .../static/doc/inner_text/get_by_css.fql | 7 + ...by_selector_all.fql => get_by_css_all.fql} | 0 .../static/doc/inner_text/get_by_selector.fql | 10 - .../static/doc/inner_text/get_by_xpath.fql | 7 + .../doc/inner_text/get_by_xpath_all.fql | 18 + e2e/tests/static/doc/xpath/attr.fql | 4 +- .../static/element/attrs/get_by_xpath.fql | 10 + .../children/{count.fql => count_by_css.fql} | 5 +- .../element/children/count_by_xpath.fql | 8 + .../children/{get.fql => get_by_css.fql} | 0 .../static/element/children/get_by_xpath.fql | 9 + .../query/{exists.fql => exists_by_css.fql} | 0 .../static/element/query/exists_by_xpath.fql | 9 + pkg/drivers/cdp/dom/document.go | 86 ++--- pkg/drivers/cdp/dom/element.go | 246 ++----------- pkg/drivers/cdp/dom/helpers.go | 55 --- pkg/drivers/cdp/dom/manager.go | 8 - pkg/drivers/cdp/eval/function.go | 5 + pkg/drivers/cdp/eval/resolver.go | 172 ++++++++++ pkg/drivers/cdp/eval/runtime.go | 111 +++--- pkg/drivers/cdp/eval/types.go | 40 +++ pkg/drivers/cdp/input/manager.go | 105 +----- pkg/drivers/cdp/input/quad.go | 37 -- pkg/drivers/cdp/network/manager.go | 2 +- pkg/drivers/cdp/templates/blur.go | 34 +- .../cdp/templates/{global.go => document.go} | 12 + pkg/drivers/cdp/templates/helpers.go | 83 +---- pkg/drivers/cdp/templates/inner_html.go | 65 +++- pkg/drivers/cdp/templates/inner_text.go | 76 ++-- pkg/drivers/cdp/templates/query.go | 80 ++++- pkg/drivers/cdp/templates/scroll.go | 43 ++- pkg/drivers/cdp/templates/select.go | 49 ++- pkg/drivers/cdp/templates/wait.go | 167 ++++++--- pkg/drivers/cdp/templates/xpath.go | 24 +- pkg/drivers/common/getter.go | 2 +- pkg/drivers/helpers.go | 12 + pkg/drivers/http/document.go | 10 +- pkg/drivers/http/element.go | 324 +++++++++++------- pkg/drivers/http/element_test.go | 4 +- pkg/drivers/http/helpers.go | 44 +-- pkg/drivers/http/xpath.go | 122 +++++++ pkg/drivers/selector.go | 115 +++++++ pkg/drivers/type.go | 28 +- pkg/drivers/value.go | 62 ++-- pkg/runtime/values/array.go | 20 ++ pkg/stdlib/html/attr_query.go | 8 +- pkg/stdlib/html/blur.go | 8 +- pkg/stdlib/html/clear.go | 8 +- pkg/stdlib/html/click.go | 20 +- pkg/stdlib/html/click_all.go | 6 +- pkg/stdlib/html/element.go | 13 +- pkg/stdlib/html/focus.go | 8 +- pkg/stdlib/html/get_inner_html.go | 5 +- pkg/stdlib/html/get_inner_html_all.go | 7 +- pkg/stdlib/html/get_inner_text.go | 5 +- pkg/stdlib/html/get_inner_text_all.go | 7 +- pkg/stdlib/html/hover.go | 5 +- pkg/stdlib/html/input.go | 18 +- pkg/stdlib/html/lib.go | 1 + pkg/stdlib/html/pagination.go | 9 +- pkg/stdlib/html/press_selector.go | 6 +- pkg/stdlib/html/scroll_element.go | 18 +- pkg/stdlib/html/select.go | 7 +- pkg/stdlib/html/set_inner_html.go | 3 +- pkg/stdlib/html/set_inner_text.go | 5 +- pkg/stdlib/html/wait_attr.go | 14 +- pkg/stdlib/html/wait_attr_all.go | 4 +- pkg/stdlib/html/wait_class.go | 14 +- pkg/stdlib/html/wait_class_all.go | 3 +- pkg/stdlib/html/wait_element.go | 9 +- pkg/stdlib/html/wait_style.go | 14 +- pkg/stdlib/html/wait_style_all.go | 3 +- pkg/stdlib/html/xpath_selector.go | 20 ++ 167 files changed, 2228 insertions(+), 1055 deletions(-) rename e2e/tests/dynamic/doc/click/{click_by_selector.fql => click_by_css.fql} (75%) rename e2e/tests/dynamic/{element/click/click_by_selector_with_count.fql => doc/click/click_by_css_with_count.fql} (85%) create mode 100644 e2e/tests/dynamic/doc/click/click_by_xpath.fql create mode 100644 e2e/tests/dynamic/doc/click/click_by_xpath_with_count.fql rename e2e/tests/dynamic/doc/element/{exists.fql => exists_by_css.fql} (74%) create mode 100644 e2e/tests/dynamic/doc/element/exists_by_xpath.fql create mode 100644 e2e/tests/dynamic/doc/focus/focus_by_css.fql create mode 100644 e2e/tests/dynamic/doc/focus/focus_by_xpath.fql rename e2e/tests/dynamic/doc/inner_html/{get_by_selector.fql => get_by_css.fql} (100%) rename e2e/tests/dynamic/doc/inner_html/{get_by_selector_all.fql => get_by_css_all.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/inner_html/get_by_xpath.fql create mode 100644 e2e/tests/dynamic/doc/inner_html/get_by_xpath_all.fql rename e2e/tests/dynamic/doc/inner_text/{get_by_selector.fql => get_by_css.fql} (100%) rename e2e/tests/dynamic/doc/inner_text/{get_by_selector_all.fql => get_by_css_all.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/inner_text/get_by_xpath.fql create mode 100644 e2e/tests/dynamic/doc/inner_text/get_by_xpath_all.fql rename e2e/tests/dynamic/doc/input/{input.fql => input_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/input/input_by_xpath.fql rename e2e/tests/dynamic/doc/{pagination.fql => pagination_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/pagination_by_xpath.fql rename e2e/tests/dynamic/doc/select/{multi.fql => multi_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/select/multi_by_xpath.fql rename e2e/tests/dynamic/doc/select/{single.fql => single_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/select/single_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{attr_all.fql => attr_all_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/attr_all_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{attr.fql => attr_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/attr_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{class_all.fql => class_all_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/class_all_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{class.fql => class_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/class_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{element.fql => element_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/element_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{no_attr.fql => no_attr_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/no_attr_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{no_class_all.fql => no_class_all_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/no_class_all_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{no_class.fql => no_class_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/no_class_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{no_element.fql => no_element_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/no_element_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{no_style_all.fql => no_style_all_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/no_style_all_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{no_style.fql => no_style_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/no_style_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{style_all.fql => style_all_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/style_all_by_xpath.fql rename e2e/tests/dynamic/doc/wait/{style.fql => style_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/doc/wait/style_by_xpath.fql rename e2e/tests/dynamic/element/blur/{blur_by_selector.fql => blur_by_css.fql} (95%) create mode 100644 e2e/tests/dynamic/element/blur/blur_by_xpath.fql rename e2e/tests/dynamic/element/clear/{clear_by_selector.fql => clear_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/element/clear/clear_by_xpath.fql rename e2e/tests/dynamic/element/click/{click_by_selector.fql => click_by_css.fql} (94%) rename e2e/tests/dynamic/{doc/click/click_by_selector_with_count.fql => element/click/click_by_css_with_count.fql} (100%) create mode 100644 e2e/tests/dynamic/element/click/click_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/click/click_by_xpath_with_count.fql rename e2e/tests/dynamic/element/focus/{focus_by_selector.fql => focus_by_css.fql} (93%) rename e2e/tests/dynamic/{doc/focus/selector.fql => element/focus/focus_by_xpath.fql} (70%) rename e2e/tests/dynamic/element/hover/{hover_by_selector.fql => hover_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/element/hover/hover_by_xpath.fql rename e2e/tests/dynamic/element/inner_html/{get_by_selector.fql => get_by_css.fql} (100%) rename e2e/tests/dynamic/element/inner_html/{get_by_selector_not_found.fail.fql => get_by_css_not_found.fail.fql} (100%) create mode 100644 e2e/tests/dynamic/element/inner_html/get_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/inner_html/get_by_xpath_not_found.fail.fql rename e2e/tests/dynamic/element/inner_html/{set_by_selector.fql => set_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/element/inner_html/set_by_xpath.fql rename e2e/tests/dynamic/element/inner_text/{get_by_selector.fql => get_by_css.fql} (100%) rename e2e/tests/dynamic/element/inner_text/{get_by_selector_not_found.fail.fql => get_by_css_not_found.fail.fql} (100%) create mode 100644 e2e/tests/dynamic/element/inner_text/get_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/inner_text/get_by_xpath_not_found.fail.fql rename e2e/tests/dynamic/element/inner_text/{set_by_selector.fail.fql => set_by_css.fail.fql} (100%) rename e2e/tests/dynamic/element/inner_text/{set_by_selector.fql => set_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/element/inner_text/set_by_xpath.fail.fql create mode 100644 e2e/tests/dynamic/element/inner_text/set_by_xpath.fql rename e2e/tests/dynamic/element/input/{input_by_selector.fql => input_by_css.fql} (100%) rename e2e/tests/dynamic/element/input/{input_by_selector_with_timeout.fql => input_by_css_with_timeout.fql} (100%) create mode 100644 e2e/tests/dynamic/element/input/input_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/input/input_by_xpath_with_timeout.fql rename e2e/tests/dynamic/element/press/{press_selector.fql => press_by_css.fql} (100%) create mode 100644 e2e/tests/dynamic/element/press/press_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/query/element_by_css.fql create mode 100644 e2e/tests/dynamic/element/query/element_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/query/elements_by_css.fql create mode 100644 e2e/tests/dynamic/element/query/elements_by_xpath.fql create mode 100644 e2e/tests/dynamic/element/query/exists_by_xpath.fql rename e2e/tests/static/doc/element/{exists.fql => exists_by_css.fql} (100%) create mode 100644 e2e/tests/static/doc/element/exists_by_xpath.fql rename e2e/tests/static/doc/inner_html/{get_by_selector.fql => get_by_css.fql} (100%) rename e2e/tests/static/doc/inner_html/{get_by_selector_all.fql => get_by_css_all.fql} (100%) create mode 100644 e2e/tests/static/doc/inner_html/get_by_xpath.fql create mode 100644 e2e/tests/static/doc/inner_html/get_by_xpath_all.fql create mode 100644 e2e/tests/static/doc/inner_text/get_by_css.fql rename e2e/tests/static/doc/inner_text/{get_by_selector_all.fql => get_by_css_all.fql} (100%) delete mode 100644 e2e/tests/static/doc/inner_text/get_by_selector.fql create mode 100644 e2e/tests/static/doc/inner_text/get_by_xpath.fql create mode 100644 e2e/tests/static/doc/inner_text/get_by_xpath_all.fql create mode 100644 e2e/tests/static/element/attrs/get_by_xpath.fql rename e2e/tests/static/element/children/{count.fql => count_by_css.fql} (50%) create mode 100644 e2e/tests/static/element/children/count_by_xpath.fql rename e2e/tests/static/element/children/{get.fql => get_by_css.fql} (100%) create mode 100644 e2e/tests/static/element/children/get_by_xpath.fql rename e2e/tests/static/element/query/{exists.fql => exists_by_css.fql} (100%) create mode 100644 e2e/tests/static/element/query/exists_by_xpath.fql create mode 100644 pkg/drivers/cdp/eval/resolver.go create mode 100644 pkg/drivers/cdp/eval/types.go rename pkg/drivers/cdp/templates/{global.go => document.go} (53%) create mode 100644 pkg/drivers/http/xpath.go create mode 100644 pkg/drivers/selector.go create mode 100644 pkg/stdlib/html/xpath_selector.go diff --git a/e2e/tests/dynamic/doc/click/click_by_selector.fql b/e2e/tests/dynamic/doc/click/click_by_css.fql similarity index 75% rename from e2e/tests/dynamic/doc/click/click_by_selector.fql rename to e2e/tests/dynamic/doc/click/click_by_css.fql index 9a4efbd9..dfb75c77 100644 --- a/e2e/tests/dynamic/doc/click/click_by_selector.fql +++ b/e2e/tests/dynamic/doc/click/click_by_css.fql @@ -1,7 +1,7 @@ LET url = @lab.cdn.dynamic + "/#/events" LET page = DOCUMENT(url, true) -CLICK(page, "#wait-class-random button") +T::TRUE(CLICK(page, "#wait-class-random button")) WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000) diff --git a/e2e/tests/dynamic/element/click/click_by_selector_with_count.fql b/e2e/tests/dynamic/doc/click/click_by_css_with_count.fql similarity index 85% rename from e2e/tests/dynamic/element/click/click_by_selector_with_count.fql rename to e2e/tests/dynamic/doc/click/click_by_css_with_count.fql index f4d6c855..7ce588b0 100644 --- a/e2e/tests/dynamic/element/click/click_by_selector_with_count.fql +++ b/e2e/tests/dynamic/doc/click/click_by_css_with_count.fql @@ -7,7 +7,7 @@ LET input = ELEMENT(page, "#text_input") INPUT(input, "Foo") -CLICK(page, "#text_input", 2) +T::TRUE(CLICK(page, "#text_input", 2)) INPUT(input, "Bar") diff --git a/e2e/tests/dynamic/doc/click/click_by_xpath.fql b/e2e/tests/dynamic/doc/click/click_by_xpath.fql new file mode 100644 index 00000000..b39d080b --- /dev/null +++ b/e2e/tests/dynamic/doc/click/click_by_xpath.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic + "/#/events" +LET page = DOCUMENT(url, true) + +T::TRUE(CLICK(page, X("//button[@id='wait-class-random-btn']"))) + +WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000) + +RETURN TRUE diff --git a/e2e/tests/dynamic/doc/click/click_by_xpath_with_count.fql b/e2e/tests/dynamic/doc/click/click_by_xpath_with_count.fql new file mode 100644 index 00000000..d7b235c9 --- /dev/null +++ b/e2e/tests/dynamic/doc/click/click_by_xpath_with_count.fql @@ -0,0 +1,16 @@ +LET url = @lab.cdn.dynamic + "/#/forms" +LET page = DOCUMENT(url, true) + +WAIT_ELEMENT(page, "form") + +LET input = ELEMENT(page, "#text_input") + +INPUT(input, "Foo") + +T::TRUE(CLICK(page, X(".//*[@id='text_input']"), 2)) + +INPUT(input, "Bar") + +WAIT(100) + +RETURN T::EQ(input.value, "Bar") \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/element/exists.fql b/e2e/tests/dynamic/doc/element/exists_by_css.fql similarity index 74% rename from e2e/tests/dynamic/doc/element/exists.fql rename to e2e/tests/dynamic/doc/element/exists_by_css.fql index 440014e9..e35c9612 100644 --- a/e2e/tests/dynamic/doc/element/exists.fql +++ b/e2e/tests/dynamic/doc/element/exists_by_css.fql @@ -1,5 +1,5 @@ LET url = @lab.cdn.dynamic -LET doc = DOCUMENT(url) +LET doc = DOCUMENT(url, { driver: "cdp" }) T::TRUE(ELEMENT_EXISTS(doc, '.text-center')) T::FALSE(ELEMENT_EXISTS(doc, '.foo-bar')) diff --git a/e2e/tests/dynamic/doc/element/exists_by_xpath.fql b/e2e/tests/dynamic/doc/element/exists_by_xpath.fql new file mode 100644 index 00000000..bd1a0b4e --- /dev/null +++ b/e2e/tests/dynamic/doc/element/exists_by_xpath.fql @@ -0,0 +1,7 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, { driver: "cdp" }) + +T::TRUE(ELEMENT_EXISTS(doc, X(".//*[contains(@class, 'text-center')]"))) +T::FALSE(ELEMENT_EXISTS(doc, X(".//*[contains(@class, 'foo-bar')]"))) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/focus/focus_by_css.fql b/e2e/tests/dynamic/doc/focus/focus_by_css.fql new file mode 100644 index 00000000..5580a825 --- /dev/null +++ b/e2e/tests/dynamic/doc/focus/focus_by_css.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic + "/#/events" +LET page = DOCUMENT(url, { driver: "cdp" }) + +FOCUS(page, "#focus-input") + +WAIT_CLASS(page, "#focus-content", "alert-success") + +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/focus/focus_by_xpath.fql b/e2e/tests/dynamic/doc/focus/focus_by_xpath.fql new file mode 100644 index 00000000..8d67b1b1 --- /dev/null +++ b/e2e/tests/dynamic/doc/focus/focus_by_xpath.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic + "/#/events" +LET page = DOCUMENT(url, { driver: "cdp" }) + +FOCUS(page, X("//*[@id='focus-input']")) + +WAIT_CLASS(page, "#focus-content", "alert-success") + +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/inner_html/get_by_selector.fql b/e2e/tests/dynamic/doc/inner_html/get_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/inner_html/get_by_selector.fql rename to e2e/tests/dynamic/doc/inner_html/get_by_css.fql diff --git a/e2e/tests/dynamic/doc/inner_html/get_by_selector_all.fql b/e2e/tests/dynamic/doc/inner_html/get_by_css_all.fql similarity index 100% rename from e2e/tests/dynamic/doc/inner_html/get_by_selector_all.fql rename to e2e/tests/dynamic/doc/inner_html/get_by_css_all.fql diff --git a/e2e/tests/dynamic/doc/inner_html/get_by_xpath.fql b/e2e/tests/dynamic/doc/inner_html/get_by_xpath.fql new file mode 100644 index 00000000..8af66797 --- /dev/null +++ b/e2e/tests/dynamic/doc/inner_html/get_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, { driver: "cdp" }) +LET selector = X('//*[@id="root"]/div/main/div') + +WAIT_ELEMENT(doc, "#layout") + +LET expected = '

Welcome to Ferret E2E test page!

It has several pages for testing different possibilities of the library

' +LET actual = INNER_HTML(doc, selector) + +RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', '')) \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/inner_html/get_by_xpath_all.fql b/e2e/tests/dynamic/doc/inner_html/get_by_xpath_all.fql new file mode 100644 index 00000000..38fc8237 --- /dev/null +++ b/e2e/tests/dynamic/doc/inner_html/get_by_xpath_all.fql @@ -0,0 +1,13 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) +LET selector = X('//*[@id="root"]/div/main/div/*') + +WAIT_ELEMENT(doc, "#layout") + +LET expected = [ +'

Welcome to Ferret E2E test page!

', +'

It has several pages for testing different possibilities of the library

' +] +LET actual = INNER_HTML_ALL(doc, selector) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/inner_text/get_by_selector.fql b/e2e/tests/dynamic/doc/inner_text/get_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/inner_text/get_by_selector.fql rename to e2e/tests/dynamic/doc/inner_text/get_by_css.fql diff --git a/e2e/tests/dynamic/doc/inner_text/get_by_selector_all.fql b/e2e/tests/dynamic/doc/inner_text/get_by_css_all.fql similarity index 100% rename from e2e/tests/dynamic/doc/inner_text/get_by_selector_all.fql rename to e2e/tests/dynamic/doc/inner_text/get_by_css_all.fql diff --git a/e2e/tests/dynamic/doc/inner_text/get_by_xpath.fql b/e2e/tests/dynamic/doc/inner_text/get_by_xpath.fql new file mode 100644 index 00000000..4818e13c --- /dev/null +++ b/e2e/tests/dynamic/doc/inner_text/get_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, { driver: "cdp" }) +LET selector = X('.//*[@id="root"]/div/main/div/*/h1') + +WAIT_ELEMENT(doc, "#layout") + +LET expected = 'Welcome to Ferret E2E test page!' +LET actual = INNER_TEXT(doc, selector) + +RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', '')) \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/inner_text/get_by_xpath_all.fql b/e2e/tests/dynamic/doc/inner_text/get_by_xpath_all.fql new file mode 100644 index 00000000..9973460d --- /dev/null +++ b/e2e/tests/dynamic/doc/inner_text/get_by_xpath_all.fql @@ -0,0 +1,16 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) +LET selector = X('.//*[@id="root"]/div/main/div/*') + +WAIT_ELEMENT(doc, "#layout") + +LET expected = [ + 'Welcome to Ferret E2E test page!', + 'It has several pages for testing different possibilities of the library' +] +LET actual = ( + FOR str IN INNER_TEXT_ALL(doc, selector) + RETURN REGEX_REPLACE(TRIM(str), '\n', '') +) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/input/input.fql b/e2e/tests/dynamic/doc/input/input_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/input/input.fql rename to e2e/tests/dynamic/doc/input/input_by_css.fql diff --git a/e2e/tests/dynamic/doc/input/input_by_xpath.fql b/e2e/tests/dynamic/doc/input/input_by_xpath.fql new file mode 100644 index 00000000..04b57b8c --- /dev/null +++ b/e2e/tests/dynamic/doc/input/input_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "form") + +LET output = ELEMENT(doc, "#text_output") + +INPUT(doc, X("//*[@id='text_input']"), "foo") + +RETURN T::EQ(output.innerText, "foo") \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/pagination.fql b/e2e/tests/dynamic/doc/pagination_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/pagination.fql rename to e2e/tests/dynamic/doc/pagination_by_css.fql diff --git a/e2e/tests/dynamic/doc/pagination_by_xpath.fql b/e2e/tests/dynamic/doc/pagination_by_xpath.fql new file mode 100644 index 00000000..2b91d39a --- /dev/null +++ b/e2e/tests/dynamic/doc/pagination_by_xpath.fql @@ -0,0 +1,9 @@ +LET url = @lab.cdn.dynamic + "/#/pagination" +LET page = DOCUMENT(url, true) + +LET items = ( + FOR i IN PAGINATION(page, X("//li[contains(@class, 'page-item-next') and contains(@class, 'page-item') and not(contains(@class, 'disabled'))]")) + RETURN i +) + +RETURN T::LEN(items, 5) \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/select/multi.fql b/e2e/tests/dynamic/doc/select/multi_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/select/multi.fql rename to e2e/tests/dynamic/doc/select/multi_by_css.fql diff --git a/e2e/tests/dynamic/doc/select/multi_by_xpath.fql b/e2e/tests/dynamic/doc/select/multi_by_xpath.fql new file mode 100644 index 00000000..4834cf14 --- /dev/null +++ b/e2e/tests/dynamic/doc/select/multi_by_xpath.fql @@ -0,0 +1,12 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "form") + +LET output = ELEMENT(doc, "#multi_select_output") +LET result = SELECT(doc, X("//*[@id='multi_select_input']"), ["1", "2", "4"]) + +T::EQ(output.innerText, "1, 2, 4") +T::EQ(JSON_STRINGIFY(result), '["1","2","4"]') + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/select/single.fql b/e2e/tests/dynamic/doc/select/single_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/select/single.fql rename to e2e/tests/dynamic/doc/select/single_by_css.fql diff --git a/e2e/tests/dynamic/doc/select/single_by_xpath.fql b/e2e/tests/dynamic/doc/select/single_by_xpath.fql new file mode 100644 index 00000000..4d49d8a4 --- /dev/null +++ b/e2e/tests/dynamic/doc/select/single_by_xpath.fql @@ -0,0 +1,12 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, { driver: "cdp" }) + +WAIT_ELEMENT(doc, "form") + +LET output = ELEMENT(doc, "#select_output") +LET result = SELECT(doc, X("//*[@id='select_input']"), ["4"]) + +T::EQ(output.innerText, "4") +T::EQ(JSON_STRINGIFY(result), '["4"]') + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/attr_all.fql b/e2e/tests/dynamic/doc/wait/attr_all_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/attr_all.fql rename to e2e/tests/dynamic/doc/wait/attr_all_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/attr_all_by_xpath.fql b/e2e/tests/dynamic/doc/wait/attr_all_by_xpath.fql new file mode 100644 index 00000000..69a82c97 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/attr_all_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn") +T::LEN(ELEMENTS(doc, X("//*[@id='wait-class-content' or @id='wait-class-random-content']")), 2) +WAIT_ATTR_ALL(doc, X("//*[@id='wait-class-content' or @id='wait-class-random-content']"), "class", "alert alert-success", 10000) + +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/attr.fql b/e2e/tests/dynamic/doc/wait/attr_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/attr.fql rename to e2e/tests/dynamic/doc/wait/attr_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/attr_by_xpath.fql b/e2e/tests/dynamic/doc/wait/attr_by_xpath.fql new file mode 100644 index 00000000..6f776203 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/attr_by_xpath.fql @@ -0,0 +1,20 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET selector = X("//*[@id='wait-class-btn']") +LET attrName = "data-ferret-x" +LET attrVal = "foobar" + +WAIT_ELEMENT(doc, "#page-events") + +LET el = ELEMENT(doc, selector) +LET prev = el.attributes + +ATTR_SET(el, attrName, attrVal) +WAIT_ATTR(doc, selector, attrName, attrVal, 30000) + +LET curr = el.attributes + +T::NONE(prev[attrName]) +T::EQ(attrVal, curr[attrName], "attributes should be updated") + +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/class_all.fql b/e2e/tests/dynamic/doc/wait/class_all_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/class_all.fql rename to e2e/tests/dynamic/doc/wait/class_all_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/class_all_by_xpath.fql b/e2e/tests/dynamic/doc/wait/class_all_by_xpath.fql new file mode 100644 index 00000000..85e57848 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/class_all_by_xpath.fql @@ -0,0 +1,9 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +CLICK_ALL(doc, "#wait-class-btn, #wait-class-random-btn") +WAIT_CLASS_ALL(doc, X("//*[@id='wait-class-content' or @id='wait-class-random-content']"), "alert-success", 10000) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/class.fql b/e2e/tests/dynamic/doc/wait/class_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/class.fql rename to e2e/tests/dynamic/doc/wait/class_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/class_by_xpath.fql b/e2e/tests/dynamic/doc/wait/class_by_xpath.fql new file mode 100644 index 00000000..0daaa3ae --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/class_by_xpath.fql @@ -0,0 +1,14 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +// with fixed timeout +CLICK(doc, "#wait-class-btn") +WAIT_CLASS(doc, X("//*[@id='wait-class-content']"), "alert-success") + +// with random timeout +CLICK(doc, "#wait-class-random-btn") +WAIT_CLASS(doc, X("//*[@id='wait-class-random-content']"), "alert-success", 10000) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/element.fql b/e2e/tests/dynamic/doc/wait/element_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/element.fql rename to e2e/tests/dynamic/doc/wait/element_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/element_by_xpath.fql b/e2e/tests/dynamic/doc/wait/element_by_xpath.fql new file mode 100644 index 00000000..dfc3cf66 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/element_by_xpath.fql @@ -0,0 +1,15 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET pageSelector = X("//*[@id='page-events']") +LET elemSelector = X("//*[@id='wait-element-content']") +LET btnSelector = "#wait-element-btn" + +WAIT_ELEMENT(doc, pageSelector) + +CLICK(doc, btnSelector) + +WAIT_ELEMENT(doc, elemSelector, 10000) + +T::TRUE(ELEMENT_EXISTS(doc, elemSelector), "element not found") + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/frame_navigation.fqlx b/e2e/tests/dynamic/doc/wait/frame_navigation.fqlx index c392e857..3fed8caa 100644 --- a/e2e/tests/dynamic/doc/wait/frame_navigation.fqlx +++ b/e2e/tests/dynamic/doc/wait/frame_navigation.fqlx @@ -1,13 +1,8 @@ LET url = @lab.cdn.dynamic + "?redirect=/iframe&src=/iframe" -// LET url = "http://192.168.4.23:8080/?redirect=/iframe&src=/iframe" LET page = DOCUMENT(url, { driver: 'cdp' }) LET original = FIRST(FRAMES(page, "url", "/\?redirect=/iframe$")) INPUT(original, "#url_input", "https://getbootstrap.com/") -// WAIT(3000) - -// LET btn = ELEMENT(original, "#submit") -// CLICK(btn) CLICK(original, "#submit") diff --git a/e2e/tests/dynamic/doc/wait/no_attr.fql b/e2e/tests/dynamic/doc/wait/no_attr_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/no_attr.fql rename to e2e/tests/dynamic/doc/wait/no_attr_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/no_attr_by_xpath.fql b/e2e/tests/dynamic/doc/wait/no_attr_by_xpath.fql new file mode 100644 index 00000000..5f619dbc --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/no_attr_by_xpath.fql @@ -0,0 +1,16 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +// with fixed timeout +CLICK(doc, "#wait-no-class-btn") +WAIT(1000) +PRINT(ATTR_GET(ELEMENT(doc, "#wait-no-class-content"), "class")) +WAIT_NO_ATTR(doc, X("//*[@id='wait-no-class-content']"), "class", "alert alert-success") + +// with random timeout +CLICK(doc, "#wait-no-class-random-btn") +WAIT_NO_ATTR(doc, X("//*[@id='wait-no-class-random-content']"), "class", "alert alert-success", 10000) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/no_class_all.fql b/e2e/tests/dynamic/doc/wait/no_class_all_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/no_class_all.fql rename to e2e/tests/dynamic/doc/wait/no_class_all_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/no_class_all_by_xpath.fql b/e2e/tests/dynamic/doc/wait/no_class_all_by_xpath.fql new file mode 100644 index 00000000..fce61615 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/no_class_all_by_xpath.fql @@ -0,0 +1,9 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +CLICK_ALL(doc, "#wait-no-class-btn, #wait-no-class-random-btn") +WAIT_NO_CLASS_ALL(doc, X("//*[@id='wait-no-class-content' or @id='wait-no-class-random-content']"), "alert-success", 10000) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/no_class.fql b/e2e/tests/dynamic/doc/wait/no_class_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/no_class.fql rename to e2e/tests/dynamic/doc/wait/no_class_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/no_class_by_xpath.fql b/e2e/tests/dynamic/doc/wait/no_class_by_xpath.fql new file mode 100644 index 00000000..4ec9178a --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/no_class_by_xpath.fql @@ -0,0 +1,14 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +// with fixed timeout +CLICK(doc, "#wait-no-class-btn") +WAIT_NO_CLASS(doc, X("//*[@id='wait-no-class-content']"), "alert-success") + +// with random timeout +CLICK(doc, "#wait-no-class-random-btn") +WAIT_NO_CLASS(doc, X("//*[@id='wait-no-class-random-content']"), "alert-success", 10000) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/no_element.fql b/e2e/tests/dynamic/doc/wait/no_element_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/no_element.fql rename to e2e/tests/dynamic/doc/wait/no_element_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/no_element_by_xpath.fql b/e2e/tests/dynamic/doc/wait/no_element_by_xpath.fql new file mode 100644 index 00000000..7d1fd530 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/no_element_by_xpath.fql @@ -0,0 +1,15 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET pageSelector = "#page-events" +LET elemSelector = X("//*[@id='wait-no-element-content']") +LET btnSelector = "#wait-no-element-btn" + +WAIT_ELEMENT(doc, pageSelector) + +CLICK(doc, btnSelector) + +WAIT_NO_ELEMENT(doc, elemSelector, 10000) + +T::FALSE(ELEMENT_EXISTS(doc, elemSelector), "element should not be found") + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/no_style_all.fql b/e2e/tests/dynamic/doc/wait/no_style_all_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/no_style_all.fql rename to e2e/tests/dynamic/doc/wait/no_style_all_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/no_style_all_by_xpath.fql b/e2e/tests/dynamic/doc/wait/no_style_all_by_xpath.fql new file mode 100644 index 00000000..4835ee47 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/no_style_all_by_xpath.fql @@ -0,0 +1,32 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET selector = X("//*[@id='wait-class-btn' or @id='wait-class-random-btn']") + +WAIT_ELEMENT(doc, "#page-events") + +LET n = ( + FOR el IN ELEMENTS(doc, selector) + ATTR_SET(el, "style", "color: black") + + RETURN NONE +) + +WAIT_STYLE_ALL(doc, selector, "color", "rgb(0, 0, 0)", 10000) + +LET n2 = ( + FOR el IN ELEMENTS(doc, selector) + ATTR_SET(el, "style", "color: red") + + RETURN NONE +) + +WAIT_NO_STYLE_ALL(doc, selector, "color", "rgb(0, 0, 0)", 10000) + +LET results = ( + FOR el IN ELEMENTS(doc, selector) + RETURN el.style.color +) + +T::EQ(results, ["rgb(255, 0, 0)","rgb(255, 0, 0)"]) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/no_style.fql b/e2e/tests/dynamic/doc/wait/no_style_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/no_style.fql rename to e2e/tests/dynamic/doc/wait/no_style_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/no_style_by_xpath.fql b/e2e/tests/dynamic/doc/wait/no_style_by_xpath.fql new file mode 100644 index 00000000..b387fef4 --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/no_style_by_xpath.fql @@ -0,0 +1,24 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET selector = X("//*[@id='wait-class-btn']") + +WAIT_ELEMENT(doc, "#page-events") + +LET el = ELEMENT(doc, selector) + +STYLE_SET(el, "color", "green") +WAIT(200) + +WAIT_STYLE(doc, selector, "color", "rgb(0, 128, 0)") + +LET prev = el.style + +STYLE_SET(el, "color", "red") +WAIT_NO_STYLE(doc, selector, "color", "rgb(0, 128, 0)") +WAIT_STYLE(doc, selector, "color", "rgb(255, 0, 0)") +LET curr = el.style + +T::EQ(prev.color, "rgb(0, 128, 0)") +T::EQ(curr.color, "rgb(255, 0, 0)") + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/style_all.fql b/e2e/tests/dynamic/doc/wait/style_all_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/style_all.fql rename to e2e/tests/dynamic/doc/wait/style_all_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/style_all_by_xpath.fql b/e2e/tests/dynamic/doc/wait/style_all_by_xpath.fql new file mode 100644 index 00000000..79a9aa3c --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/style_all_by_xpath.fql @@ -0,0 +1,23 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET selector = X("//*[@id='wait-class-btn' or @id='wait-class-random-btn']") + +WAIT_ELEMENT(doc, "#page-events") + +LET n = ( + FOR el IN ELEMENTS(doc, selector) + ATTR_SET(el, "style", "width: 200px") + + RETURN NONE +) + +WAIT_STYLE_ALL(doc, selector, "width", "200px", 10000) + +LET results = ( + FOR el IN ELEMENTS(doc, selector) + RETURN el.style.width +) + +T::EQ(results, ["200px","200px"]) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/wait/style.fql b/e2e/tests/dynamic/doc/wait/style_by_css.fql similarity index 100% rename from e2e/tests/dynamic/doc/wait/style.fql rename to e2e/tests/dynamic/doc/wait/style_by_css.fql diff --git a/e2e/tests/dynamic/doc/wait/style_by_xpath.fql b/e2e/tests/dynamic/doc/wait/style_by_xpath.fql new file mode 100644 index 00000000..363da71f --- /dev/null +++ b/e2e/tests/dynamic/doc/wait/style_by_xpath.fql @@ -0,0 +1,18 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) +LET selector = X("//*[@id='wait-class-btn']") + +WAIT_ELEMENT(doc, "#page-events") + +LET el = ELEMENT(doc, selector) +LET prev = el.style + +ATTR_SET(el, "style", "width: 200px") +WAIT_STYLE(doc, selector, "width", "200px") + +LET curr = el.style + +T::NOT::EQ(prev.width, "200px") +T::EQ(curr.width, "200px") + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/element/blur/blur_by_selector.fql b/e2e/tests/dynamic/element/blur/blur_by_css.fql similarity index 95% rename from e2e/tests/dynamic/element/blur/blur_by_selector.fql rename to e2e/tests/dynamic/element/blur/blur_by_css.fql index 5eb38b44..426379e0 100644 --- a/e2e/tests/dynamic/element/blur/blur_by_selector.fql +++ b/e2e/tests/dynamic/element/blur/blur_by_css.fql @@ -9,4 +9,4 @@ BLUR(page, "#focus-input") WAIT_NO_CLASS(page, "#focus-content", "alert-success") -RETURN "" \ No newline at end of file +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/element/blur/blur_by_xpath.fql b/e2e/tests/dynamic/element/blur/blur_by_xpath.fql new file mode 100644 index 00000000..0f0dea01 --- /dev/null +++ b/e2e/tests/dynamic/element/blur/blur_by_xpath.fql @@ -0,0 +1,12 @@ +LET url = @lab.cdn.dynamic + "/#/events" +LET page = DOCUMENT(url, true) + +FOCUS(page, "#focus-input") + +WAIT_CLASS(page, "#focus-content", "alert-success") + +BLUR(page, X('.//*[@id="focus-input"]')) + +WAIT_NO_CLASS(page, "#focus-content", "alert-success") + +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/element/clear/clear_by_selector.fql b/e2e/tests/dynamic/element/clear/clear_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/clear/clear_by_selector.fql rename to e2e/tests/dynamic/element/clear/clear_by_css.fql diff --git a/e2e/tests/dynamic/element/clear/clear_by_xpath.fql b/e2e/tests/dynamic/element/clear/clear_by_xpath.fql new file mode 100644 index 00000000..35e9392a --- /dev/null +++ b/e2e/tests/dynamic/element/clear/clear_by_xpath.fql @@ -0,0 +1,28 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "form") + +LET form = ELEMENT(doc, "#page-form") + +INPUT(form, "#text_input", "foo") +INPUT_CLEAR(form, X('//*[@id="text_input"]')) + +LET input = ELEMENT(doc, "#text_input") +LET output = ELEMENT(doc, "#text_output") + +T::EMPTY(output.innerText) + +INPUT(form, "#text_input", "test0-test1", 100) + +INPUT_CLEAR(form, X('//*[@id="text_input"]')) + +T::EMPTY(output.innerText) + +INPUT(form, "#text_input", "test0&test1", 100) + +INPUT_CLEAR(form, X('//*[@id="text_input"]')) + +T::EMPTY(output.innerText) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/element/click/click_by_selector.fql b/e2e/tests/dynamic/element/click/click_by_css.fql similarity index 94% rename from e2e/tests/dynamic/element/click/click_by_selector.fql rename to e2e/tests/dynamic/element/click/click_by_css.fql index 09466cea..d79840c9 100644 --- a/e2e/tests/dynamic/element/click/click_by_selector.fql +++ b/e2e/tests/dynamic/element/click/click_by_css.fql @@ -7,4 +7,4 @@ CLICK(div, "button") WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000) -RETURN "" +RETURN TRUE diff --git a/e2e/tests/dynamic/doc/click/click_by_selector_with_count.fql b/e2e/tests/dynamic/element/click/click_by_css_with_count.fql similarity index 100% rename from e2e/tests/dynamic/doc/click/click_by_selector_with_count.fql rename to e2e/tests/dynamic/element/click/click_by_css_with_count.fql diff --git a/e2e/tests/dynamic/element/click/click_by_xpath.fql b/e2e/tests/dynamic/element/click/click_by_xpath.fql new file mode 100644 index 00000000..7e39e5e1 --- /dev/null +++ b/e2e/tests/dynamic/element/click/click_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.dynamic + "/#/events" +LET page = DOCUMENT(url, true) + +LET div = ELEMENT(page, "#wait-class-random") + +CLICK(div, X(".//button")) + +WAIT_CLASS(page, "#wait-class-random-content", "alert-success", 10000) + +RETURN TRUE diff --git a/e2e/tests/dynamic/element/click/click_by_xpath_with_count.fql b/e2e/tests/dynamic/element/click/click_by_xpath_with_count.fql new file mode 100644 index 00000000..840f522c --- /dev/null +++ b/e2e/tests/dynamic/element/click/click_by_xpath_with_count.fql @@ -0,0 +1,16 @@ +LET url = @lab.cdn.dynamic + "/#/forms" +LET page = DOCUMENT(url, true) + +WAIT_ELEMENT(page, "form") + +LET input = ELEMENT(page, "#text_input") + +INPUT(input, "Foo") + +CLICK(page, X('.//*[@id="text_input"]'), 2) + +INPUT(input, "Bar") + +WAIT(100) + +RETURN T::EQ(input.value, "Bar") \ No newline at end of file diff --git a/e2e/tests/dynamic/element/focus/focus_by_selector.fql b/e2e/tests/dynamic/element/focus/focus_by_css.fql similarity index 93% rename from e2e/tests/dynamic/element/focus/focus_by_selector.fql rename to e2e/tests/dynamic/element/focus/focus_by_css.fql index 378e76a1..b548c7d2 100644 --- a/e2e/tests/dynamic/element/focus/focus_by_selector.fql +++ b/e2e/tests/dynamic/element/focus/focus_by_css.fql @@ -5,4 +5,4 @@ FOCUS(page, "#focus-input") WAIT_CLASS(page, "#focus-content", "alert-success") -RETURN "" \ No newline at end of file +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/doc/focus/selector.fql b/e2e/tests/dynamic/element/focus/focus_by_xpath.fql similarity index 70% rename from e2e/tests/dynamic/doc/focus/selector.fql rename to e2e/tests/dynamic/element/focus/focus_by_xpath.fql index 378e76a1..582af793 100644 --- a/e2e/tests/dynamic/doc/focus/selector.fql +++ b/e2e/tests/dynamic/element/focus/focus_by_xpath.fql @@ -1,8 +1,8 @@ LET url = @lab.cdn.dynamic + "/#/events" LET page = DOCUMENT(url, true) -FOCUS(page, "#focus-input") +FOCUS(page, X('//*[@id="focus-input"]')) WAIT_CLASS(page, "#focus-content", "alert-success") -RETURN "" \ No newline at end of file +RETURN TRUE \ No newline at end of file diff --git a/e2e/tests/dynamic/element/hover/hover_by_selector.fql b/e2e/tests/dynamic/element/hover/hover_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/hover/hover_by_selector.fql rename to e2e/tests/dynamic/element/hover/hover_by_css.fql diff --git a/e2e/tests/dynamic/element/hover/hover_by_xpath.fql b/e2e/tests/dynamic/element/hover/hover_by_xpath.fql new file mode 100644 index 00000000..d173e454 --- /dev/null +++ b/e2e/tests/dynamic/element/hover/hover_by_xpath.fql @@ -0,0 +1,11 @@ +LET url = @lab.cdn.dynamic + "?redirect=/events" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "#page-events") + +HOVER(doc, X('.//*[@id="hoverable-btn"]')) +WAIT_ELEMENT(doc, "#hoverable-content") + +LET output = INNER_TEXT(doc, "#hoverable-content") + +RETURN T::EQ(output, "Lorem ipsum dolor sit amet.") \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_html/get_by_selector.fql b/e2e/tests/dynamic/element/inner_html/get_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_html/get_by_selector.fql rename to e2e/tests/dynamic/element/inner_html/get_by_css.fql diff --git a/e2e/tests/dynamic/element/inner_html/get_by_selector_not_found.fail.fql b/e2e/tests/dynamic/element/inner_html/get_by_css_not_found.fail.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_html/get_by_selector_not_found.fail.fql rename to e2e/tests/dynamic/element/inner_html/get_by_css_not_found.fail.fql diff --git a/e2e/tests/dynamic/element/inner_html/get_by_xpath.fql b/e2e/tests/dynamic/element/inner_html/get_by_xpath.fql new file mode 100644 index 00000000..16eb6cc8 --- /dev/null +++ b/e2e/tests/dynamic/element/inner_html/get_by_xpath.fql @@ -0,0 +1,34 @@ +LET url = @lab.cdn.dynamic + "/#/lists" +LET doc = DOCUMENT(url, true) + +LET expected = [ + {"details":'
Lil Tecca
Ransom'}, + {"details":'
NLE Choppa
Shotta Flow (Feat. Blueface) [Remix]'}, + {"details":'
Baby Jesus (DaBaby)
Suge'}, + {"details":'
NLE Choppa
Shotta Flow 3'}, + {"details":'
Lil Tecca
Lil Tecca - Did It Again'}, + {"details":'
NLE Choppa
Shotta Flow'}, + {"details":'
Ynw Melly
Dangerously In Love (772 Love Pt. 2)'}, + {"details":'
POLO G
Polo G feat. Lil TJay - Pop Out'}, + {"details":`
MUSTARD
Ballin' (feat. Roddy Ricch)`}, + {"details":'
Lil Nas X
Panini'}, + {"details":'
Juice WRLD
Juice Wrld - RUN'}, + {"details":'
Shordie Shordie
Betchua (Bitchuary)'}, + {"details":'
Post Malone
Goodbyes (feat. Young Thug)'}, + {"details":'
LIL UZI VERT
Sanguine Paradise'}, + {"details":'
Calboy
Envy Me'}, + {"details":'
Ambjaay
Uno'}, + {"details":'
Lil Tecca
Lil Tecca - Bossanova'}, + {"details":'
Lil Baby
Baby'}, + {"details":'
Lil Tjay
Lil Tjay - Brothers (Prod by JDONTHATRACK & Protegebeatz)'}, + {"details":'
YK Osiris
Worth It'} +] + +LET actual = ( + FOR item IN ELEMENTS(doc, '.track-list li') + RETURN { + details: INNER_HTML(item, X(".//*[contains(@class, 'track-details')]")), + } +) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_html/get_by_xpath_not_found.fail.fql b/e2e/tests/dynamic/element/inner_html/get_by_xpath_not_found.fail.fql new file mode 100644 index 00000000..be04547e --- /dev/null +++ b/e2e/tests/dynamic/element/inner_html/get_by_xpath_not_found.fail.fql @@ -0,0 +1,5 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) +LET el = ELEMENT(doc, ".jumbotron") + +RETURN INNER_HTML(el, X(".//h5")) \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_html/set_by_selector.fql b/e2e/tests/dynamic/element/inner_html/set_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_html/set_by_selector.fql rename to e2e/tests/dynamic/element/inner_html/set_by_css.fql diff --git a/e2e/tests/dynamic/element/inner_html/set_by_xpath.fql b/e2e/tests/dynamic/element/inner_html/set_by_xpath.fql new file mode 100644 index 00000000..7010d959 --- /dev/null +++ b/e2e/tests/dynamic/element/inner_html/set_by_xpath.fql @@ -0,0 +1,25 @@ +LET url = @lab.cdn.dynamic + "/#/lists" +LET doc = DOCUMENT(url, true) + +LET expected = [ + {"details":'
MEDUZA
Piece Of Your Heart (feat. Goodboys)'}, + {"details":'
Metanoia Music
Che Crozz x Orbis - Lift Me Up'} +] + +LET html = ( + FOR t IN expected + RETURN '
  • ' + t.details + '
  • ' +) + +INNER_HTML_SET(doc, X(".//*[contains(@class, 'track-list')]"), CONCAT_SEPARATOR('\n', html)) + +LET list = ELEMENT(doc, '.track-list') + +LET actual = ( + FOR item IN ELEMENTS(doc, '.track-list li') + RETURN { + details: INNER_HTML(item, '.track-details'), + } +) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_text/get_by_selector.fql b/e2e/tests/dynamic/element/inner_text/get_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_text/get_by_selector.fql rename to e2e/tests/dynamic/element/inner_text/get_by_css.fql diff --git a/e2e/tests/dynamic/element/inner_text/get_by_selector_not_found.fail.fql b/e2e/tests/dynamic/element/inner_text/get_by_css_not_found.fail.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_text/get_by_selector_not_found.fail.fql rename to e2e/tests/dynamic/element/inner_text/get_by_css_not_found.fail.fql diff --git a/e2e/tests/dynamic/element/inner_text/get_by_xpath.fql b/e2e/tests/dynamic/element/inner_text/get_by_xpath.fql new file mode 100644 index 00000000..14eb3859 --- /dev/null +++ b/e2e/tests/dynamic/element/inner_text/get_by_xpath.fql @@ -0,0 +1,35 @@ + +LET url = @lab.cdn.dynamic + "/#/lists" +LET doc = DOCUMENT(url, true) + +LET expected = [ + {"artist":"Lil Tecca","track":"Ransom"}, + {"artist":"NLE Choppa","track":"Shotta Flow (Feat. Blueface) [Remix]"}, + {"artist":"Baby Jesus (DaBaby)","track":"Suge"}, + {"artist":"NLE Choppa","track":"Shotta Flow 3"}, + {"artist":"Lil Tecca","track":"Lil Tecca - Did It Again"}, + {"artist":"NLE Choppa","track":"Shotta Flow"}, + {"artist":"Ynw Melly","track":"Dangerously In Love (772 Love Pt. 2)"}, + {"artist":"POLO G","track":"Polo G feat. Lil TJay - Pop Out"}, + {"artist":"MUSTARD","track":"Ballin' (feat. Roddy Ricch)"}, + {"artist":"Lil Nas X","track":"Panini"}, + {"artist":"Juice WRLD","track":"Juice Wrld - RUN"}, + {"artist":"Shordie Shordie","track":"Betchua (Bitchuary)"}, + {"artist":"Post Malone","track":"Goodbyes (feat. Young Thug)"}, + {"artist":"LIL UZI VERT","track":"Sanguine Paradise"}, + {"artist":"Calboy","track":"Envy Me"}, + {"artist":"Ambjaay","track":"Uno"}, + {"artist":"Lil Tecca","track":"Lil Tecca - Bossanova"}, + {"artist":"Lil Baby","track":"Baby"}, + {"artist":"Lil Tjay","track":"Lil Tjay - Brothers (Prod by JDONTHATRACK & Protegebeatz)"}, + {"artist":"YK Osiris","track":"Worth It"} +] +LET actual = ( + FOR item IN ELEMENTS(doc, '.track-list li') + RETURN { + artist: TRIM(INNER_TEXT(item, X(".//*[contains(@class, 'track-artist')]"))), + track: TRIM(INNER_TEXT(item, X(".//*[contains(@class, 'track-name')]"))) + } +) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_text/get_by_xpath_not_found.fail.fql b/e2e/tests/dynamic/element/inner_text/get_by_xpath_not_found.fail.fql new file mode 100644 index 00000000..36b619b1 --- /dev/null +++ b/e2e/tests/dynamic/element/inner_text/get_by_xpath_not_found.fail.fql @@ -0,0 +1,5 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) +LET el = ELEMENT(doc, ".jumbotron") + +RETURN INNER_TEXT(el, X(".//h5")) \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_text/set_by_selector.fail.fql b/e2e/tests/dynamic/element/inner_text/set_by_css.fail.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_text/set_by_selector.fail.fql rename to e2e/tests/dynamic/element/inner_text/set_by_css.fail.fql diff --git a/e2e/tests/dynamic/element/inner_text/set_by_selector.fql b/e2e/tests/dynamic/element/inner_text/set_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/inner_text/set_by_selector.fql rename to e2e/tests/dynamic/element/inner_text/set_by_css.fql diff --git a/e2e/tests/dynamic/element/inner_text/set_by_xpath.fail.fql b/e2e/tests/dynamic/element/inner_text/set_by_xpath.fail.fql new file mode 100644 index 00000000..a67fc24d --- /dev/null +++ b/e2e/tests/dynamic/element/inner_text/set_by_xpath.fail.fql @@ -0,0 +1,5 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) +LET el = ELEMENT(doc, ".jumbotron") + +RETURN INNER_TEXT_SET(el, X(".//h4"), "foobar") \ No newline at end of file diff --git a/e2e/tests/dynamic/element/inner_text/set_by_xpath.fql b/e2e/tests/dynamic/element/inner_text/set_by_xpath.fql new file mode 100644 index 00000000..5fbc288c --- /dev/null +++ b/e2e/tests/dynamic/element/inner_text/set_by_xpath.fql @@ -0,0 +1,37 @@ +LET url = @lab.cdn.dynamic + "/#/lists" +LET doc = DOCUMENT(url, true) + +LET expected = [ + { + "artist":'MEDUZA', + "track": 'Piece Of Your Heart (feat. Goodboys)' + }, + { + "artist": 'Metanoia Music', + "track": 'Che Crozz x Orbis - Lift Me Up' + } +] + +LET f = ( + FOR item, idx IN ELEMENTS(doc, '.track-list li') + LIMIT 2 + LET value = expected[idx] + + INNER_HTML_SET(item, X(".//*[contains(@class, 'track-artist')]"), value.artist) + INNER_HTML_SET(item, X(".//*[contains(@class, 'track-name')]"), value.track) + + RETURN NONE +) + +LET list = ELEMENT(doc, '.track-list') + +LET actual = ( + FOR item IN ELEMENTS(doc, '.track-list li') + LIMIT 2 + RETURN { + artist: TRIM(INNER_TEXT(item, '.track-artist')), + track: TRIM(INNER_TEXT(item, '.track-name')) + } +) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/dynamic/element/input/input_by_selector.fql b/e2e/tests/dynamic/element/input/input_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/input/input_by_selector.fql rename to e2e/tests/dynamic/element/input/input_by_css.fql diff --git a/e2e/tests/dynamic/element/input/input_by_selector_with_timeout.fql b/e2e/tests/dynamic/element/input/input_by_css_with_timeout.fql similarity index 100% rename from e2e/tests/dynamic/element/input/input_by_selector_with_timeout.fql rename to e2e/tests/dynamic/element/input/input_by_css_with_timeout.fql diff --git a/e2e/tests/dynamic/element/input/input_by_xpath.fql b/e2e/tests/dynamic/element/input/input_by_xpath.fql new file mode 100644 index 00000000..928fc9a5 --- /dev/null +++ b/e2e/tests/dynamic/element/input/input_by_xpath.fql @@ -0,0 +1,12 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "form") + +LET form = ELEMENT(doc, "#page-form") + +INPUT(form, X('//*[@id="text_input"]'), "foo") + +LET output = ELEMENT(doc, "#text_output") + +RETURN T::EQ(output.innerText, "foo") \ No newline at end of file diff --git a/e2e/tests/dynamic/element/input/input_by_xpath_with_timeout.fql b/e2e/tests/dynamic/element/input/input_by_xpath_with_timeout.fql new file mode 100644 index 00000000..706576c3 --- /dev/null +++ b/e2e/tests/dynamic/element/input/input_by_xpath_with_timeout.fql @@ -0,0 +1,12 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +WAIT_ELEMENT(doc, "form") + +LET form = ELEMENT(doc, "#page-form") + +INPUT(form, X('//*[@id="text_input"]'), "foo", 100) + +LET output = ELEMENT(doc, "#text_output") + +RETURN T::EQ(output.innerText, "foo") \ No newline at end of file diff --git a/e2e/tests/dynamic/element/press/press_selector.fql b/e2e/tests/dynamic/element/press/press_by_css.fql similarity index 100% rename from e2e/tests/dynamic/element/press/press_selector.fql rename to e2e/tests/dynamic/element/press/press_by_css.fql diff --git a/e2e/tests/dynamic/element/press/press_by_xpath.fql b/e2e/tests/dynamic/element/press/press_by_xpath.fql new file mode 100644 index 00000000..4ed5d511 --- /dev/null +++ b/e2e/tests/dynamic/element/press/press_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.dynamic + "/#/events" +LET page = DOCUMENT(url, true) + +PRESS_SELECTOR(page, X("//*[@id='press-input']"), "Enter") + +WAIT(100) + +T::EQ(INNER_TEXT(page, "#press-content"), "Enter") + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/dynamic/element/query/element_by_css.fql b/e2e/tests/dynamic/element/query/element_by_css.fql new file mode 100644 index 00000000..f8763dab --- /dev/null +++ b/e2e/tests/dynamic/element/query/element_by_css.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) + +LET el = ELEMENT(doc, "#root")? + +T::NOT::NONE(el) + +RETURN TRUE diff --git a/e2e/tests/dynamic/element/query/element_by_xpath.fql b/e2e/tests/dynamic/element/query/element_by_xpath.fql new file mode 100644 index 00000000..8a950522 --- /dev/null +++ b/e2e/tests/dynamic/element/query/element_by_xpath.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) + +LET el = ELEMENT(doc, X("//*[@id='root']"))? + +T::NOT::NONE(el) + +RETURN TRUE diff --git a/e2e/tests/dynamic/element/query/elements_by_css.fql b/e2e/tests/dynamic/element/query/elements_by_css.fql new file mode 100644 index 00000000..b2e1ac41 --- /dev/null +++ b/e2e/tests/dynamic/element/query/elements_by_css.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +LET els = ELEMENTS(doc, ".form-control") + +T::NOT::EMPTY(els) + +RETURN TRUE diff --git a/e2e/tests/dynamic/element/query/elements_by_xpath.fql b/e2e/tests/dynamic/element/query/elements_by_xpath.fql new file mode 100644 index 00000000..be73d6e9 --- /dev/null +++ b/e2e/tests/dynamic/element/query/elements_by_xpath.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.dynamic + "?redirect=/forms" +LET doc = DOCUMENT(url, true) + +LET els = ELEMENTS(doc, X(".//*[contains(@class, 'form-control')]")) + +T::NOT::EMPTY(els) + +RETURN TRUE diff --git a/e2e/tests/dynamic/element/query/exists_by_xpath.fql b/e2e/tests/dynamic/element/query/exists_by_xpath.fql new file mode 100644 index 00000000..41fe772e --- /dev/null +++ b/e2e/tests/dynamic/element/query/exists_by_xpath.fql @@ -0,0 +1,9 @@ +LET url = @lab.cdn.dynamic +LET doc = DOCUMENT(url, true) + +LET el = ELEMENT(doc, "#root") + +T::TRUE(ELEMENT_EXISTS(el, X(".//*[contains(@class, 'jumbotron')]"))) +T::FALSE(ELEMENT_EXISTS(el, X(".//*[contains(@class, 'foo-bar')]"))) + +RETURN NONE diff --git a/e2e/tests/static/doc/element/exists.fql b/e2e/tests/static/doc/element/exists_by_css.fql similarity index 100% rename from e2e/tests/static/doc/element/exists.fql rename to e2e/tests/static/doc/element/exists_by_css.fql diff --git a/e2e/tests/static/doc/element/exists_by_xpath.fql b/e2e/tests/static/doc/element/exists_by_xpath.fql new file mode 100644 index 00000000..267959e9 --- /dev/null +++ b/e2e/tests/static/doc/element/exists_by_xpath.fql @@ -0,0 +1,7 @@ +LET url = @lab.cdn.static + '/overview.html' +LET doc = DOCUMENT(url) + +T::TRUE(ELEMENT_EXISTS(doc, X("//[contains(@class, 'section-nav')]"))) +T::FALSE(ELEMENT_EXISTS(doc, X("//[contains(@class, 'foo-bar')]"))) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/static/doc/inner_html/get_by_selector.fql b/e2e/tests/static/doc/inner_html/get_by_css.fql similarity index 100% rename from e2e/tests/static/doc/inner_html/get_by_selector.fql rename to e2e/tests/static/doc/inner_html/get_by_css.fql diff --git a/e2e/tests/static/doc/inner_html/get_by_selector_all.fql b/e2e/tests/static/doc/inner_html/get_by_css_all.fql similarity index 100% rename from e2e/tests/static/doc/inner_html/get_by_selector_all.fql rename to e2e/tests/static/doc/inner_html/get_by_css_all.fql diff --git a/e2e/tests/static/doc/inner_html/get_by_xpath.fql b/e2e/tests/static/doc/inner_html/get_by_xpath.fql new file mode 100644 index 00000000..8e4d1c87 --- /dev/null +++ b/e2e/tests/static/doc/inner_html/get_by_xpath.fql @@ -0,0 +1,7 @@ +LET url = @lab.cdn.static + '/overview.html' +LET doc = DOCUMENT(url) + +LET expected = '
  • Containers
  • Responsive breakpoints
  • Z-index
  • ' +LET actual = INNER_HTML(doc, X("//[contains(@class, 'section-nav')]")) + +RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', '')) \ No newline at end of file diff --git a/e2e/tests/static/doc/inner_html/get_by_xpath_all.fql b/e2e/tests/static/doc/inner_html/get_by_xpath_all.fql new file mode 100644 index 00000000..8daf7917 --- /dev/null +++ b/e2e/tests/static/doc/inner_html/get_by_xpath_all.fql @@ -0,0 +1,14 @@ +LET url = @lab.cdn.static + '/overview.html' +LET doc = DOCUMENT(url) + +LET expected = [ +'Containers', +'Responsive breakpoints', +'Z-index' +] +LET actual = ( + FOR i IN INNER_HTML_ALL(doc, X("//*[contains(@class, 'section-nav')]/li")) + RETURN TRIM(i) +) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/static/doc/inner_text/get_by_css.fql b/e2e/tests/static/doc/inner_text/get_by_css.fql new file mode 100644 index 00000000..8614c3a6 --- /dev/null +++ b/e2e/tests/static/doc/inner_text/get_by_css.fql @@ -0,0 +1,7 @@ +LET url = @lab.cdn.static + '/overview.html' +LET doc = DOCUMENT(url) + +LET expected = 'ContainersResponsive breakpointsZ-index' +LET actual = INNER_TEXT(doc, '.section-nav') + +RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', '')) \ No newline at end of file diff --git a/e2e/tests/static/doc/inner_text/get_by_selector_all.fql b/e2e/tests/static/doc/inner_text/get_by_css_all.fql similarity index 100% rename from e2e/tests/static/doc/inner_text/get_by_selector_all.fql rename to e2e/tests/static/doc/inner_text/get_by_css_all.fql diff --git a/e2e/tests/static/doc/inner_text/get_by_selector.fql b/e2e/tests/static/doc/inner_text/get_by_selector.fql deleted file mode 100644 index 422a9356..00000000 --- a/e2e/tests/static/doc/inner_text/get_by_selector.fql +++ /dev/null @@ -1,10 +0,0 @@ -LET url = @lab.cdn.static + '/simple.html' -LET doc = DOCUMENT(url) - -LET expected = `Title Hello world` -LET actual = INNER_TEXT(doc) - -LET r1 = '(\s|\")' -LET r2 = '(\n|\s|\")' - -RETURN T::EQ(REGEX_REPLACE(TRIM(actual), r2, ''), REGEX_REPLACE(expected, r1, '')) \ No newline at end of file diff --git a/e2e/tests/static/doc/inner_text/get_by_xpath.fql b/e2e/tests/static/doc/inner_text/get_by_xpath.fql new file mode 100644 index 00000000..ce802bcf --- /dev/null +++ b/e2e/tests/static/doc/inner_text/get_by_xpath.fql @@ -0,0 +1,7 @@ +LET url = @lab.cdn.static + '/overview.html' +LET doc = DOCUMENT(url) + +LET expected = 'ContainersResponsive breakpointsZ-index' +LET actual = INNER_TEXT(doc, X("//*[contains(@class, 'section-nav')]")) + +RETURN T::EQ(REGEX_REPLACE(TRIM(actual), '(\n|\s)', ''), REGEX_REPLACE(expected, '\s', '')) \ No newline at end of file diff --git a/e2e/tests/static/doc/inner_text/get_by_xpath_all.fql b/e2e/tests/static/doc/inner_text/get_by_xpath_all.fql new file mode 100644 index 00000000..7b99f80b --- /dev/null +++ b/e2e/tests/static/doc/inner_text/get_by_xpath_all.fql @@ -0,0 +1,18 @@ +LET url = @lab.cdn.static + '/grid.html' +LET doc = DOCUMENT(url) + +LET expected = [ + "Containers provide a means to center and horizontally pad your site’s contents. Use .container for a responsive pixel width or .container-fluid for width: 100% across all viewport and device sizes.", + "Rows are wrappers for columns. Each column has horizontal padding (called a gutter) for controlling the space between them. This padding is then counteracted on the rows with negative margins. This way, all the content in your columns is visually aligned down the left side.", + "In a grid layout, content must be placed within columns and only columns may be immediate children of rows.", + "Thanks to flexbox, grid columns without a specified width will automatically layout as equal width columns. For example, four instances of .col-sm will each automatically be 25% wide from the small breakpoint and up. See the auto-layout columns section for more examples.", + "Column classes indicate the number of columns you’d like to use out of the possible 12 per row. So, if you want three equal-width columns across, you can use .col-4.", + "Column widths are set in percentages, so they’re always fluid and sized relative to their parent element.", + "Columns have horizontal padding to create the gutters between individual columns, however, you can remove the margin from rows and padding from columns with .no-gutters on the .row.", + "To make the grid responsive, there are five grid breakpoints, one for each responsive breakpoint: all breakpoints (extra small), small, medium, large, and extra large.", + "Grid breakpoints are based on minimum width media queries, meaning they apply to that one breakpoint and all those above it (e.g., .col-sm-4 applies to small, medium, large, and extra large devices, but not the first xs breakpoint).", + "You can use predefined grid classes (like .col-4) or Sass mixins for more semantic markup." +] +LET actual = INNER_TEXT_ALL(doc, X('//body/div[contains(@class, "container-fluid")]/div/main/ul/li')) + +RETURN T::EQ(actual, expected) \ No newline at end of file diff --git a/e2e/tests/static/doc/xpath/attr.fql b/e2e/tests/static/doc/xpath/attr.fql index 4b2b6ef7..a77f04fd 100644 --- a/e2e/tests/static/doc/xpath/attr.fql +++ b/e2e/tests/static/doc/xpath/attr.fql @@ -1,6 +1,6 @@ LET url = @lab.cdn.static + '/simple.html' LET page = DOCUMENT(url) -LET actual = XPATH(page, "//meta/@charset") +LET actual = XPATH(page, "string(//meta/@charset)") -RETURN T::EQ(actual, ["UTF-8"]) +RETURN T::EQ(actual, "UTF-8") diff --git a/e2e/tests/static/element/attrs/get_by_xpath.fql b/e2e/tests/static/element/attrs/get_by_xpath.fql new file mode 100644 index 00000000..f2f3caf7 --- /dev/null +++ b/e2e/tests/static/element/attrs/get_by_xpath.fql @@ -0,0 +1,10 @@ +LET url = @lab.cdn.static + '/overview.html' +LET doc = DOCUMENT(url) + +LET element = ELEMENT(doc, X("//body/header/a")) +LET actual = XPATH(element, "string(@href)") +LET expected = "http://getbootstrap.com/" + +T::EQ(actual, expected) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/static/element/children/count.fql b/e2e/tests/static/element/children/count_by_css.fql similarity index 50% rename from e2e/tests/static/element/children/count.fql rename to e2e/tests/static/element/children/count_by_css.fql index ebc8d90d..74068963 100644 --- a/e2e/tests/static/element/children/count.fql +++ b/e2e/tests/static/element/children/count_by_css.fql @@ -1,9 +1,8 @@ LET url = @lab.cdn.static + '/list.html' LET doc = DOCUMENT(url) -LET list = ELEMENT(doc, ".track-list") +LET len = ELEMENTS_COUNT(doc, ".track-details") -T::EQ(list.length, 20) -T::LEN(list, 20) +T::EQ(len, 20) RETURN NONE \ No newline at end of file diff --git a/e2e/tests/static/element/children/count_by_xpath.fql b/e2e/tests/static/element/children/count_by_xpath.fql new file mode 100644 index 00000000..749cb95d --- /dev/null +++ b/e2e/tests/static/element/children/count_by_xpath.fql @@ -0,0 +1,8 @@ +LET url = @lab.cdn.static + '/list.html' +LET doc = DOCUMENT(url) + +LET len = ELEMENTS_COUNT(doc, X("//*[contains(@class, 'track-details')]")) + +T::EQ(len, 20) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/static/element/children/get.fql b/e2e/tests/static/element/children/get_by_css.fql similarity index 100% rename from e2e/tests/static/element/children/get.fql rename to e2e/tests/static/element/children/get_by_css.fql diff --git a/e2e/tests/static/element/children/get_by_xpath.fql b/e2e/tests/static/element/children/get_by_xpath.fql new file mode 100644 index 00000000..e8451347 --- /dev/null +++ b/e2e/tests/static/element/children/get_by_xpath.fql @@ -0,0 +1,9 @@ +LET url = @lab.cdn.static + '/list.html' +LET doc = DOCUMENT(url) + +LET list = ELEMENT(doc, X("//*[contains(@class, 'track-list')]")) +LET children = list.children +T::NOT::NONE(children) +T::NOT::EMPTY(children) + +RETURN NONE \ No newline at end of file diff --git a/e2e/tests/static/element/query/exists.fql b/e2e/tests/static/element/query/exists_by_css.fql similarity index 100% rename from e2e/tests/static/element/query/exists.fql rename to e2e/tests/static/element/query/exists_by_css.fql diff --git a/e2e/tests/static/element/query/exists_by_xpath.fql b/e2e/tests/static/element/query/exists_by_xpath.fql new file mode 100644 index 00000000..0db0dc63 --- /dev/null +++ b/e2e/tests/static/element/query/exists_by_xpath.fql @@ -0,0 +1,9 @@ +LET url = @lab.cdn.static + '/value.html' +LET doc = DOCUMENT(url) + +LET el = ELEMENT(doc, "#listings_table") + +T::TRUE(ELEMENT_EXISTS(el, X("//*[contains(@class, 'odd')]"))) +T::FALSE(ELEMENT_EXISTS(el, X("//*[contains(@class, 'foo-bar')]"))) + +RETURN NONE \ No newline at end of file diff --git a/pkg/drivers/cdp/dom/document.go b/pkg/drivers/cdp/dom/document.go index 7dd2cc79..cf8875c4 100644 --- a/pkg/drivers/cdp/dom/document.go +++ b/pkg/drivers/cdp/dom/document.go @@ -2,10 +2,10 @@ package dom import ( "context" + "github.com/mafredri/cdp/protocol/runtime" "hash/fnv" "github.com/mafredri/cdp" - "github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/page" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -26,7 +26,7 @@ type HTMLDocument struct { client *cdp.Client dom *Manager input *input.Manager - exec *eval.Runtime + eval *eval.Runtime frameTree page.FrameTree element *HTMLElement } @@ -39,24 +39,12 @@ func LoadRootHTMLDocument( mouse *input.Mouse, keyboard *input.Keyboard, ) (*HTMLDocument, error) { - gdRepl, err := client.DOM.GetDocument(ctx, dom.NewGetDocumentArgs().SetDepth(1)) - - if err != nil { - return nil, err - } - ftRepl, err := client.Page.GetFrameTree(ctx) if err != nil { return nil, err } - exec, err := eval.New(ctx, logger, client, ftRepl.FrameTree.Frame.ID) - - if err != nil { - return nil, err - } - return LoadHTMLDocument( ctx, logger, @@ -64,9 +52,7 @@ func LoadRootHTMLDocument( domManager, mouse, keyboard, - gdRepl.Root, ftRepl.FrameTree, - exec, ) } @@ -77,21 +63,21 @@ func LoadHTMLDocument( domManager *Manager, mouse *input.Mouse, keyboard *input.Keyboard, - node dom.Node, frameTree page.FrameTree, - exec *eval.Runtime, ) (*HTMLDocument, error) { + exec, err := eval.Create(ctx, logger, client, frameTree.Frame.ID) + + if err != nil { + return nil, err + } + inputManager := input.NewManager(logger, client, exec, keyboard, mouse) - rootElement, err := LoadHTMLElement( - ctx, - logger, - client, - domManager, - inputManager, - exec, - node.NodeID, - ) + exec.SetLoader(func(ctx context.Context, remoteType eval.RemoteType, id runtime.RemoteObjectID) (core.Value, error) { + return NewHTMLElement(logger, client, domManager, inputManager, exec, id), nil + }) + + rootElement, err := exec.EvalElement(ctx, templates.GetDocument()) if err != nil { return nil, errors.Wrap(err, "failed to load root element") @@ -103,7 +89,7 @@ func LoadHTMLDocument( domManager, inputManager, exec, - rootElement, + rootElement.(*HTMLElement), frameTree, ), nil } @@ -122,7 +108,7 @@ func NewHTMLDocument( doc.client = client doc.dom = domManager doc.input = input - doc.exec = exec + doc.eval = exec doc.element = rootElement doc.frameTree = frames @@ -216,24 +202,24 @@ func (doc *HTMLDocument) GetChildNode(ctx context.Context, idx values.Int) (core return doc.element.GetChildNode(ctx, idx) } -func (doc *HTMLDocument) QuerySelector(ctx context.Context, selector values.String) (core.Value, error) { +func (doc *HTMLDocument) QuerySelector(ctx context.Context, selector drivers.QuerySelector) (core.Value, error) { return doc.element.QuerySelector(ctx, selector) } -func (doc *HTMLDocument) QuerySelectorAll(ctx context.Context, selector values.String) (*values.Array, error) { +func (doc *HTMLDocument) QuerySelectorAll(ctx context.Context, selector drivers.QuerySelector) (*values.Array, error) { return doc.element.QuerySelectorAll(ctx, selector) } -func (doc *HTMLDocument) CountBySelector(ctx context.Context, selector values.String) (values.Int, error) { +func (doc *HTMLDocument) CountBySelector(ctx context.Context, selector drivers.QuerySelector) (values.Int, error) { return doc.element.CountBySelector(ctx, selector) } -func (doc *HTMLDocument) ExistsBySelector(ctx context.Context, selector values.String) (values.Boolean, error) { +func (doc *HTMLDocument) ExistsBySelector(ctx context.Context, selector drivers.QuerySelector) (values.Boolean, error) { return doc.element.ExistsBySelector(ctx, selector) } func (doc *HTMLDocument) GetTitle() values.String { - value, err := doc.exec.ReadProperty(context.Background(), doc.element.id, "title") + value, err := doc.eval.EvalValue(context.Background(), templates.GetTitle()) if err != nil { doc.logError(errors.Wrap(err, "failed to read document title")) @@ -298,9 +284,9 @@ func (doc *HTMLDocument) MoveMouseByXY(ctx context.Context, x, y values.Float) e return doc.input.MoveMouseByXY(ctx, x, y) } -func (doc *HTMLDocument) WaitForElement(ctx context.Context, selector values.String, when drivers.WaitEvent) error { +func (doc *HTMLDocument) WaitForElement(ctx context.Context, selector drivers.QuerySelector, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForElement(doc.element.id, selector, when), events.DefaultPolling, ) @@ -310,9 +296,9 @@ func (doc *HTMLDocument) WaitForElement(ctx context.Context, selector values.Str return err } -func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error { +func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector drivers.QuerySelector, class values.String, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForClassBySelector(doc.element.id, selector, class, when), events.DefaultPolling, ) @@ -322,9 +308,9 @@ func (doc *HTMLDocument) WaitForClassBySelector(ctx context.Context, selector, c return err } -func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error { +func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector drivers.QuerySelector, class values.String, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForClassBySelectorAll(doc.element.id, selector, class, when), events.DefaultPolling, ) @@ -336,13 +322,13 @@ func (doc *HTMLDocument) WaitForClassBySelectorAll(ctx context.Context, selector func (doc *HTMLDocument) WaitForAttributeBySelector( ctx context.Context, - selector, + selector drivers.QuerySelector, name, value values.String, when drivers.WaitEvent, ) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForAttributeBySelector(doc.element.id, selector, name, value, when), events.DefaultPolling, ) @@ -354,13 +340,13 @@ func (doc *HTMLDocument) WaitForAttributeBySelector( func (doc *HTMLDocument) WaitForAttributeBySelectorAll( ctx context.Context, - selector, + selector drivers.QuerySelector, name, value values.String, when drivers.WaitEvent, ) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForAttributeBySelectorAll(doc.element.id, selector, name, value, when), events.DefaultPolling, ) @@ -370,9 +356,9 @@ func (doc *HTMLDocument) WaitForAttributeBySelectorAll( return err } -func (doc *HTMLDocument) WaitForStyleBySelector(ctx context.Context, selector, name, value values.String, when drivers.WaitEvent) error { +func (doc *HTMLDocument) WaitForStyleBySelector(ctx context.Context, selector drivers.QuerySelector, name, value values.String, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForStyleBySelector(doc.element.id, selector, name, value, when), events.DefaultPolling, ) @@ -382,9 +368,9 @@ func (doc *HTMLDocument) WaitForStyleBySelector(ctx context.Context, selector, n return err } -func (doc *HTMLDocument) WaitForStyleBySelectorAll(ctx context.Context, selector, name, value values.String, when drivers.WaitEvent) error { +func (doc *HTMLDocument) WaitForStyleBySelectorAll(ctx context.Context, selector drivers.QuerySelector, name, value values.String, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( - doc.exec, + doc.eval, templates.WaitForStyleBySelectorAll(doc.element.id, selector, name, value, when), events.DefaultPolling, ) @@ -402,7 +388,7 @@ func (doc *HTMLDocument) ScrollBottom(ctx context.Context, options drivers.Scrol return doc.input.ScrollBottom(ctx, options) } -func (doc *HTMLDocument) ScrollBySelector(ctx context.Context, selector values.String, options drivers.ScrollOptions) error { +func (doc *HTMLDocument) ScrollBySelector(ctx context.Context, selector drivers.QuerySelector, options drivers.ScrollOptions) error { return doc.input.ScrollIntoViewBySelector(ctx, doc.element.id, selector, options) } @@ -411,7 +397,7 @@ func (doc *HTMLDocument) Scroll(ctx context.Context, options drivers.ScrollOptio } func (doc *HTMLDocument) Eval(ctx context.Context, expression string) (core.Value, error) { - return doc.exec.EvalValue(ctx, eval.F(expression)) + return doc.eval.EvalValue(ctx, eval.F(expression)) } func (doc *HTMLDocument) logError(err error) *zerolog.Event { diff --git a/pkg/drivers/cdp/dom/element.go b/pkg/drivers/cdp/dom/element.go index b9106cd5..0335bc03 100644 --- a/pkg/drivers/cdp/dom/element.go +++ b/pkg/drivers/cdp/dom/element.go @@ -2,7 +2,6 @@ package dom import ( "context" - "encoding/json" "fmt" "hash/fnv" "strings" @@ -63,37 +62,13 @@ func LoadHTMLElement( return nil, core.Error(core.ErrNotFound, fmt.Sprintf("element %s", ref.Object.Value)) } - return ResolveHTMLElement( - logger, - client, - domManager, - input, - exec, - ref.Object, - ) -} - -func ResolveHTMLElement( - logger zerolog.Logger, - client *cdp.Client, - domManager *Manager, - input *input.Manager, - exec *eval.Runtime, - ref runtime.RemoteObject, -) (*HTMLElement, error) { - if ref.ObjectID == nil { - return nil, core.Error(core.ErrNotFound, fmt.Sprintf("element %s", ref.Value)) - } - - id := *ref.ObjectID - return NewHTMLElement( logger, client, domManager, input, exec, - id, + *ref.Object.ObjectID, ), nil } @@ -286,59 +261,35 @@ func (el *HTMLElement) RemoveAttribute(ctx context.Context, names ...values.Stri } func (el *HTMLElement) GetChildNodes(ctx context.Context) (*values.Array, error) { - out, err := el.evalTo(ctx, templates.GetChildren(el.id)) - - if err != nil { - return values.EmptyArray(), err - } - - return values.ToArray(ctx, out), nil + return el.exec.EvalElements(ctx, templates.GetChildren(el.id)) } func (el *HTMLElement) GetChildNode(ctx context.Context, idx values.Int) (core.Value, error) { - return el.evalToElement(ctx, templates.GetChildByIndex(el.id, idx)) + return el.exec.EvalElement(ctx, templates.GetChildByIndex(el.id, idx)) } func (el *HTMLElement) GetParentElement(ctx context.Context) (core.Value, error) { - return el.evalToElement(ctx, templates.GetParent(el.id)) + return el.exec.EvalElement(ctx, templates.GetParent(el.id)) } func (el *HTMLElement) GetPreviousElementSibling(ctx context.Context) (core.Value, error) { - return el.evalToElement(ctx, templates.GetPreviousElementSibling(el.id)) + return el.exec.EvalElement(ctx, templates.GetPreviousElementSibling(el.id)) } func (el *HTMLElement) GetNextElementSibling(ctx context.Context) (core.Value, error) { - return el.evalToElement(ctx, templates.GetNextElementSibling(el.id)) + return el.exec.EvalElement(ctx, templates.GetNextElementSibling(el.id)) } -func (el *HTMLElement) QuerySelector(ctx context.Context, selector values.String) (core.Value, error) { - return el.evalToElement(ctx, templates.QuerySelector(el.id, selector)) +func (el *HTMLElement) QuerySelector(ctx context.Context, selector drivers.QuerySelector) (core.Value, error) { + return el.exec.EvalElement(ctx, templates.QuerySelector(el.id, selector)) } -func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector values.String) (*values.Array, error) { - out, err := el.exec.EvalRef(ctx, templates.QuerySelectorAll(el.id, selector)) - - if err != nil { - return values.EmptyArray(), err - } - - res, err := el.fromEvalRef(ctx, out) - - if err != nil { - return values.EmptyArray(), err - } - - return values.ToArray(ctx, res), nil +func (el *HTMLElement) QuerySelectorAll(ctx context.Context, selector drivers.QuerySelector) (*values.Array, error) { + return el.exec.EvalElements(ctx, templates.QuerySelectorAll(el.id, selector)) } func (el *HTMLElement) XPath(ctx context.Context, expression values.String) (result core.Value, err error) { - out, err := el.exec.EvalRef(ctx, templates.XPath(el.id, expression)) - - if err != nil { - return values.None, err - } - - return el.fromEvalRef(ctx, out) + return el.exec.EvalValue(ctx, templates.XPath(el.id, expression)) } func (el *HTMLElement) GetInnerText(ctx context.Context) (values.String, error) { @@ -358,7 +309,7 @@ func (el *HTMLElement) SetInnerText(ctx context.Context, innerText values.String ) } -func (el *HTMLElement) GetInnerTextBySelector(ctx context.Context, selector values.String) (values.String, error) { +func (el *HTMLElement) GetInnerTextBySelector(ctx context.Context, selector drivers.QuerySelector) (values.String, error) { out, err := el.exec.EvalValue(ctx, templates.GetInnerTextBySelector(el.id, selector)) if err != nil { @@ -368,14 +319,14 @@ func (el *HTMLElement) GetInnerTextBySelector(ctx context.Context, selector valu return values.ToString(out), nil } -func (el *HTMLElement) SetInnerTextBySelector(ctx context.Context, selector, innerText values.String) error { +func (el *HTMLElement) SetInnerTextBySelector(ctx context.Context, selector drivers.QuerySelector, innerText values.String) error { return el.exec.Eval( ctx, templates.SetInnerTextBySelector(el.id, selector, innerText), ) } -func (el *HTMLElement) GetInnerTextBySelectorAll(ctx context.Context, selector values.String) (*values.Array, error) { +func (el *HTMLElement) GetInnerTextBySelectorAll(ctx context.Context, selector drivers.QuerySelector) (*values.Array, error) { out, err := el.exec.EvalValue(ctx, templates.GetInnerTextBySelectorAll(el.id, selector)) if err != nil { @@ -399,7 +350,7 @@ func (el *HTMLElement) SetInnerHTML(ctx context.Context, innerHTML values.String return el.exec.Eval(ctx, templates.SetInnerHTML(el.id, innerHTML)) } -func (el *HTMLElement) GetInnerHTMLBySelector(ctx context.Context, selector values.String) (values.String, error) { +func (el *HTMLElement) GetInnerHTMLBySelector(ctx context.Context, selector drivers.QuerySelector) (values.String, error) { out, err := el.exec.EvalValue(ctx, templates.GetInnerHTMLBySelector(el.id, selector)) if err != nil { @@ -409,11 +360,11 @@ func (el *HTMLElement) GetInnerHTMLBySelector(ctx context.Context, selector valu return values.ToString(out), nil } -func (el *HTMLElement) SetInnerHTMLBySelector(ctx context.Context, selector, innerHTML values.String) error { +func (el *HTMLElement) SetInnerHTMLBySelector(ctx context.Context, selector drivers.QuerySelector, innerHTML values.String) error { return el.exec.Eval(ctx, templates.SetInnerHTMLBySelector(el.id, selector, innerHTML)) } -func (el *HTMLElement) GetInnerHTMLBySelectorAll(ctx context.Context, selector values.String) (*values.Array, error) { +func (el *HTMLElement) GetInnerHTMLBySelectorAll(ctx context.Context, selector drivers.QuerySelector) (*values.Array, error) { out, err := el.exec.EvalValue(ctx, templates.GetInnerHTMLBySelectorAll(el.id, selector)) if err != nil { @@ -423,7 +374,7 @@ func (el *HTMLElement) GetInnerHTMLBySelectorAll(ctx context.Context, selector v return values.ToArray(ctx, out), nil } -func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.String) (values.Int, error) { +func (el *HTMLElement) CountBySelector(ctx context.Context, selector drivers.QuerySelector) (values.Int, error) { out, err := el.exec.EvalValue(ctx, templates.CountBySelector(el.id, selector)) if err != nil { @@ -433,7 +384,7 @@ func (el *HTMLElement) CountBySelector(ctx context.Context, selector values.Stri return values.ToInt(out), nil } -func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector values.String) (values.Boolean, error) { +func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector drivers.QuerySelector) (values.Boolean, error) { out, err := el.exec.EvalValue(ctx, templates.ExistsBySelector(el.id, selector)) if err != nil { @@ -443,7 +394,7 @@ func (el *HTMLElement) ExistsBySelector(ctx context.Context, selector values.Str return values.ToBoolean(out), nil } -func (el *HTMLElement) WaitForElement(ctx context.Context, selector values.String, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForElement(ctx context.Context, selector drivers.QuerySelector, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForElement(el.id, selector, when), @@ -455,7 +406,7 @@ func (el *HTMLElement) WaitForElement(ctx context.Context, selector values.Strin return err } -func (el *HTMLElement) WaitForElementAll(ctx context.Context, selector values.String, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForElementAll(ctx context.Context, selector drivers.QuerySelector, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForElementAll(el.id, selector, when), @@ -479,7 +430,7 @@ func (el *HTMLElement) WaitForClass(ctx context.Context, class values.String, wh return err } -func (el *HTMLElement) WaitForClassBySelector(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForClassBySelector(ctx context.Context, selector drivers.QuerySelector, class values.String, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForClassBySelector(el.id, selector, class, when), @@ -491,7 +442,7 @@ func (el *HTMLElement) WaitForClassBySelector(ctx context.Context, selector, cla return err } -func (el *HTMLElement) WaitForClassBySelectorAll(ctx context.Context, selector, class values.String, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForClassBySelectorAll(ctx context.Context, selector drivers.QuerySelector, class values.String, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForClassBySelectorAll(el.id, selector, class, when), @@ -520,7 +471,7 @@ func (el *HTMLElement) WaitForAttribute( return err } -func (el *HTMLElement) WaitForAttributeBySelector(ctx context.Context, selector, name values.String, value core.Value, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForAttributeBySelector(ctx context.Context, selector drivers.QuerySelector, name values.String, value core.Value, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForAttributeBySelector(el.id, selector, name, value, when), @@ -532,7 +483,7 @@ func (el *HTMLElement) WaitForAttributeBySelector(ctx context.Context, selector, return err } -func (el *HTMLElement) WaitForAttributeBySelectorAll(ctx context.Context, selector, name values.String, value core.Value, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForAttributeBySelectorAll(ctx context.Context, selector drivers.QuerySelector, name values.String, value core.Value, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForAttributeBySelectorAll(el.id, selector, name, value, when), @@ -556,7 +507,7 @@ func (el *HTMLElement) WaitForStyle(ctx context.Context, name values.String, val return err } -func (el *HTMLElement) WaitForStyleBySelector(ctx context.Context, selector, name values.String, value core.Value, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForStyleBySelector(ctx context.Context, selector drivers.QuerySelector, name values.String, value core.Value, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForStyleBySelector(el.id, selector, name, value, when), @@ -568,7 +519,7 @@ func (el *HTMLElement) WaitForStyleBySelector(ctx context.Context, selector, nam return err } -func (el *HTMLElement) WaitForStyleBySelectorAll(ctx context.Context, selector, name values.String, value core.Value, when drivers.WaitEvent) error { +func (el *HTMLElement) WaitForStyleBySelectorAll(ctx context.Context, selector drivers.QuerySelector, name values.String, value core.Value, when drivers.WaitEvent) error { task := events.NewEvalWaitTask( el.exec, templates.WaitForStyleBySelectorAll(el.id, selector, name, value, when), @@ -584,11 +535,11 @@ func (el *HTMLElement) Click(ctx context.Context, count values.Int) error { return el.input.Click(ctx, el.id, int(count)) } -func (el *HTMLElement) ClickBySelector(ctx context.Context, selector values.String, count values.Int) error { +func (el *HTMLElement) ClickBySelector(ctx context.Context, selector drivers.QuerySelector, count values.Int) error { return el.input.ClickBySelector(ctx, el.id, selector, count) } -func (el *HTMLElement) ClickBySelectorAll(ctx context.Context, selector values.String, count values.Int) error { +func (el *HTMLElement) ClickBySelectorAll(ctx context.Context, selector drivers.QuerySelector, count values.Int) error { elements, err := el.QuerySelectorAll(ctx, selector) if err != nil { @@ -627,7 +578,7 @@ func (el *HTMLElement) Input(ctx context.Context, value core.Value, delay values }) } -func (el *HTMLElement) InputBySelector(ctx context.Context, selector values.String, value core.Value, delay values.Int) error { +func (el *HTMLElement) InputBySelector(ctx context.Context, selector drivers.QuerySelector, value core.Value, delay values.Int) error { return el.input.TypeBySelector(ctx, el.id, selector, input.TypeParams{ Text: value.String(), Clear: false, @@ -639,7 +590,7 @@ func (el *HTMLElement) Press(ctx context.Context, keys []values.String, count va return el.input.Press(ctx, values.UnwrapStrings(keys), int(count)) } -func (el *HTMLElement) PressBySelector(ctx context.Context, selector values.String, keys []values.String, count values.Int) error { +func (el *HTMLElement) PressBySelector(ctx context.Context, selector drivers.QuerySelector, keys []values.String, count values.Int) error { return el.input.PressBySelector(ctx, el.id, selector, values.UnwrapStrings(keys), int(count)) } @@ -647,7 +598,7 @@ func (el *HTMLElement) Clear(ctx context.Context) error { return el.input.Clear(ctx, el.id) } -func (el *HTMLElement) ClearBySelector(ctx context.Context, selector values.String) error { +func (el *HTMLElement) ClearBySelector(ctx context.Context, selector drivers.QuerySelector) error { return el.input.ClearBySelector(ctx, el.id, selector) } @@ -655,7 +606,7 @@ func (el *HTMLElement) Select(ctx context.Context, value *values.Array) (*values return el.input.Select(ctx, el.id, value) } -func (el *HTMLElement) SelectBySelector(ctx context.Context, selector values.String, value *values.Array) (*values.Array, error) { +func (el *HTMLElement) SelectBySelector(ctx context.Context, selector drivers.QuerySelector, value *values.Array) (*values.Array, error) { return el.input.SelectBySelector(ctx, el.id, selector, value) } @@ -667,7 +618,7 @@ func (el *HTMLElement) Focus(ctx context.Context) error { return el.input.Focus(ctx, el.id) } -func (el *HTMLElement) FocusBySelector(ctx context.Context, selector values.String) error { +func (el *HTMLElement) FocusBySelector(ctx context.Context, selector drivers.QuerySelector) error { return el.input.FocusBySelector(ctx, el.id, selector) } @@ -675,7 +626,7 @@ func (el *HTMLElement) Blur(ctx context.Context) error { return el.input.Blur(ctx, el.id) } -func (el *HTMLElement) BlurBySelector(ctx context.Context, selector values.String) error { +func (el *HTMLElement) BlurBySelector(ctx context.Context, selector drivers.QuerySelector) error { return el.input.BlurBySelector(ctx, el.id, selector) } @@ -683,135 +634,10 @@ func (el *HTMLElement) Hover(ctx context.Context) error { return el.input.MoveMouse(ctx, el.id) } -func (el *HTMLElement) HoverBySelector(ctx context.Context, selector values.String) error { +func (el *HTMLElement) HoverBySelector(ctx context.Context, selector drivers.QuerySelector) error { return el.input.MoveMouseBySelector(ctx, el.id, selector) } -func (el *HTMLElement) evalTo(ctx context.Context, fn *eval.Function) (core.Value, error) { - out, err := el.exec.EvalRef(ctx, fn) - - if err != nil { - return values.None, err - } - - return el.fromEvalRef(ctx, out) -} - -func (el *HTMLElement) evalToElement(ctx context.Context, fn *eval.Function) (core.Value, error) { - obj, err := el.exec.EvalRef(ctx, fn) - - if err != nil { - return values.None, err - } - - if obj.Type != "object" || obj.ObjectID == nil { - return values.None, nil - } - - return ResolveHTMLElement( - el.logger, - el.client, - el.dom, - el.input, - el.exec, - obj, - ) -} - -func (el *HTMLElement) fromEvalRef(ctx context.Context, out runtime.RemoteObject) (core.Value, error) { - var subtype string - - if out.Subtype != nil { - subtype = *out.Subtype - } - - if subtype == "null" || subtype == "undefined" { - return values.None, nil - } - - switch subtype { - case "array", "HTMLCollection", "NodeList": - if out.ObjectID == nil { - return values.None, nil - } - - props, err := el.client.Runtime.GetProperties(ctx, runtime.NewGetPropertiesArgs(*out.ObjectID).SetOwnProperties(true)) - - if err != nil { - return values.None, err - } - - if props.ExceptionDetails != nil { - exception := *props.ExceptionDetails - - return values.None, errors.New(exception.Text) - } - - result := values.NewArray(len(props.Result)) - - for _, descr := range props.Result { - if !descr.Enumerable { - continue - } - - if descr.Value == nil { - continue - } - - // it's not a Node, it's an attr value - if descr.Value.ObjectID == nil { - var value interface{} - - if err := json.Unmarshal(descr.Value.Value, &value); err != nil { - return values.None, err - } - - result.Push(values.Parse(value)) - - continue - } - - el, err := ResolveHTMLElement( - el.logger, - el.client, - el.dom, - el.input, - el.exec, - *descr.Value, - ) - - if err != nil { - return values.None, err - } - - result.Push(el) - } - - return result, nil - case "node": - if out.ObjectID == nil { - var value interface{} - - if err := json.Unmarshal(out.Value, &value); err != nil { - return values.None, err - } - - return values.Parse(value), nil - } - - return ResolveHTMLElement( - el.logger, - el.client, - el.dom, - el.input, - el.exec, - out, - ) - default: - return eval.Unmarshal(out) - } -} - func (el *HTMLElement) logError(err error) *zerolog.Event { return el.logger. Error(). diff --git a/pkg/drivers/cdp/dom/helpers.go b/pkg/drivers/cdp/dom/helpers.go index 6d210756..9f9f369c 100644 --- a/pkg/drivers/cdp/dom/helpers.go +++ b/pkg/drivers/cdp/dom/helpers.go @@ -2,67 +2,12 @@ package dom import ( "bytes" - "context" - "github.com/rs/zerolog" "regexp" "strings" - - "github.com/mafredri/cdp" - "github.com/mafredri/cdp/protocol/dom" - "github.com/mafredri/cdp/protocol/page" - "github.com/pkg/errors" - - "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" ) var camelMatcher = regexp.MustCompile("[A-Za-z0-9]+") -func resolveFrame(ctx context.Context, logger zerolog.Logger, client *cdp.Client, frameID page.FrameID) (dom.Node, *eval.Runtime, error) { - exec, err := eval.New(ctx, logger, client, frameID) - - if err != nil { - return dom.Node{}, nil, errors.Wrap(err, "create JS executor") - } - - evalRes, err := exec.EvalRef(ctx, eval.F("return document")) - - if err != nil { - return dom.Node{}, nil, err - } - - if evalRes.ObjectID == nil { - return dom.Node{}, nil, errors.New("failed to resolve frame document") - } - - req, err := client.DOM.RequestNode(ctx, dom.NewRequestNodeArgs(*evalRes.ObjectID)) - - if err != nil { - return dom.Node{}, nil, err - } - - if req.NodeID == 0 { - return dom.Node{}, nil, errors.New("framed document is resolved with empty node id") - } - - desc, err := client.DOM.DescribeNode( - ctx, - dom. - NewDescribeNodeArgs(). - SetNodeID(req.NodeID). - SetDepth(1), - ) - - if err != nil { - return dom.Node{}, nil, err - } - - // Returned node, by some reason, does not contain the NodeID - // So, we have to set it manually - desc.Node.NodeID = req.NodeID - - return desc.Node, exec, nil -} - func toCamelCase(input string) string { var buf bytes.Buffer diff --git a/pkg/drivers/cdp/dom/manager.go b/pkg/drivers/cdp/dom/manager.go index 98c2c2ce..f62db582 100644 --- a/pkg/drivers/cdp/dom/manager.go +++ b/pkg/drivers/cdp/dom/manager.go @@ -213,12 +213,6 @@ func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (* } // the frames is not loaded yet - node, exec, err := resolveFrame(ctx, m.logger, m.client, frameID) - - if err != nil { - return nil, errors.Wrapf(err, "failed to resolve frame node: %s", frameID) - } - doc, err := LoadHTMLDocument( ctx, m.logger, @@ -226,9 +220,7 @@ func (m *Manager) getFrameInternal(ctx context.Context, frameID page.FrameID) (* m, m.mouse, m.keyboard, - node, frame.tree, - exec, ) if err != nil { diff --git a/pkg/drivers/cdp/eval/function.go b/pkg/drivers/cdp/eval/function.go index 2f6dc88f..7ee0d0c0 100644 --- a/pkg/drivers/cdp/eval/function.go +++ b/pkg/drivers/cdp/eval/function.go @@ -1,6 +1,7 @@ package eval import ( + "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/runtime/core" "github.com/mafredri/cdp/protocol/runtime" "github.com/rs/zerolog" @@ -74,6 +75,10 @@ func (fn *Function) WithArgValue(value core.Value) *Function { }) } +func (fn *Function) WithArgSelector(selector drivers.QuerySelector) *Function { + return fn.WithArg(selector.String()) +} + func (fn *Function) WithArg(value interface{}) *Function { raw, err := jettison.MarshalOpts(value, jettison.NoHTMLEscaping()) diff --git a/pkg/drivers/cdp/eval/resolver.go b/pkg/drivers/cdp/eval/resolver.go new file mode 100644 index 00000000..5996f77a --- /dev/null +++ b/pkg/drivers/cdp/eval/resolver.go @@ -0,0 +1,172 @@ +package eval + +import ( + "context" + "github.com/MontFerret/ferret/pkg/drivers" + "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/mafredri/cdp" + "github.com/mafredri/cdp/protocol/runtime" + "github.com/pkg/errors" +) + +type ( + ValueLoader func(ctx context.Context, remoteType RemoteType, id runtime.RemoteObjectID) (core.Value, error) + + Resolver struct { + runtime cdp.Runtime + loader ValueLoader + } +) + +func NewResolver(runtime cdp.Runtime) *Resolver { + return &Resolver{runtime, nil} +} + +func (r *Resolver) SetLoader(loader ValueLoader) *Resolver { + r.loader = loader + + return r +} + +func (r *Resolver) ToValue(ctx context.Context, ref runtime.RemoteObject) (core.Value, error) { + // It's not an actual ref but rather a plain value + if ref.ObjectID == nil { + return values.Unmarshal(ref.Value) + } + + switch ToRemoteType(ref) { + case NullType, UndefinedType: + return values.None, nil + case ArrayType: + props, err := r.runtime.GetProperties(ctx, runtime.NewGetPropertiesArgs(*ref.ObjectID).SetOwnProperties(true)) + + if err != nil { + return values.None, err + } + + if props.ExceptionDetails != nil { + exception := *props.ExceptionDetails + + return values.None, errors.New(exception.Text) + } + + result := values.NewArray(len(props.Result)) + + for _, descr := range props.Result { + if !descr.Enumerable { + continue + } + + if descr.Value == nil { + continue + } + + el, err := r.ToValue(ctx, *descr.Value) + + if err != nil { + return values.None, err + } + + result.Push(el) + } + + return result, nil + case NodeType: + // could it be possible? + if ref.ObjectID == nil { + return values.Unmarshal(ref.Value) + } + + return r.loadValue(ctx, NodeType, *ref.ObjectID) + default: + return Unmarshal(ref) + } +} + +func (r *Resolver) ToElement(ctx context.Context, ref runtime.RemoteObject) (drivers.HTMLElement, error) { + if ref.ObjectID == nil { + return nil, core.Error(core.ErrInvalidArgument, "ref id") + } + + val, err := r.loadValue(ctx, ToRemoteType(ref), *ref.ObjectID) + + if err != nil { + return nil, err + } + + return drivers.ToElement(val) +} + +func (r *Resolver) ToProperty( + ctx context.Context, + id runtime.RemoteObjectID, + propName string, +) (core.Value, error) { + res, err := r.runtime.GetProperties( + ctx, + runtime.NewGetPropertiesArgs(id), + ) + + if err != nil { + return values.None, err + } + + if err := parseRuntimeException(res.ExceptionDetails); err != nil { + return values.None, err + } + + for _, prop := range res.Result { + if prop.Name == propName { + if prop.Value != nil { + return r.ToValue(ctx, *prop.Value) + } + + return values.None, nil + } + } + + return values.None, nil +} + +func (r *Resolver) ToProperties( + ctx context.Context, + id runtime.RemoteObjectID, +) (*values.Array, error) { + res, err := r.runtime.GetProperties( + ctx, + runtime.NewGetPropertiesArgs(id), + ) + + if err != nil { + return values.EmptyArray(), err + } + + if err := parseRuntimeException(res.ExceptionDetails); err != nil { + return values.EmptyArray(), err + } + + arr := values.NewArray(len(res.Result)) + + for _, prop := range res.Result { + if prop.Value != nil { + val, err := r.ToValue(ctx, *prop.Value) + + if err != nil { + return values.EmptyArray(), err + } + + arr.Push(val) + } + } + + return arr, nil +} + +func (r *Resolver) loadValue(ctx context.Context, remoteType RemoteType, id runtime.RemoteObjectID) (core.Value, error) { + if r.loader == nil { + return values.None, core.Error(core.ErrNotImplemented, "ValueLoader") + } + + return r.loader(ctx, remoteType, id) +} diff --git a/pkg/drivers/cdp/eval/runtime.go b/pkg/drivers/cdp/eval/runtime.go index 9b3f1492..eec50bad 100644 --- a/pkg/drivers/cdp/eval/runtime.go +++ b/pkg/drivers/cdp/eval/runtime.go @@ -2,15 +2,16 @@ package eval import ( "context" - "github.com/MontFerret/ferret/pkg/runtime/logging" - "github.com/rs/zerolog" "github.com/mafredri/cdp" "github.com/mafredri/cdp/protocol/page" "github.com/mafredri/cdp/protocol/runtime" "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/logging" "github.com/MontFerret/ferret/pkg/runtime/values" ) @@ -21,23 +22,40 @@ type Runtime struct { client *cdp.Client frame page.Frame contextID runtime.ExecutionContextID + resolver *Resolver } -func New(ctx context.Context, logger zerolog.Logger, client *cdp.Client, frameID page.FrameID) (*Runtime, error) { +func Create( + ctx context.Context, + logger zerolog.Logger, + client *cdp.Client, + frameID page.FrameID, +) (*Runtime, error) { world, err := client.Page.CreateIsolatedWorld(ctx, page.NewCreateIsolatedWorldArgs(frameID)) if err != nil { return nil, err } - return Create(logger, client, world.ExecutionContextID), nil + return New(logger, client, world.ExecutionContextID), nil } -func Create(logger zerolog.Logger, client *cdp.Client, contextID runtime.ExecutionContextID) *Runtime { +func New( + logger zerolog.Logger, + client *cdp.Client, + contextID runtime.ExecutionContextID, +) *Runtime { rt := new(Runtime) rt.logger = logging.WithName(logger.With(), "js-eval").Logger() rt.client = client rt.contextID = contextID + rt.resolver = NewResolver(client.Runtime) + + return rt +} + +func (rt *Runtime) SetLoader(loader ValueLoader) *Runtime { + rt.resolver.SetLoader(loader) return rt } @@ -52,16 +70,6 @@ func (rt *Runtime) Eval(ctx context.Context, fn *Function) error { return err } -func (rt *Runtime) EvalValue(ctx context.Context, fn *Function) (core.Value, error) { - out, err := rt.call(ctx, fn.returnValue()) - - if err != nil { - return values.None, err - } - - return CastToValue(out) -} - func (rt *Runtime) EvalRef(ctx context.Context, fn *Function) (runtime.RemoteObject, error) { out, err := rt.call(ctx, fn.returnRef()) @@ -69,60 +77,52 @@ func (rt *Runtime) EvalRef(ctx context.Context, fn *Function) (runtime.RemoteObj return runtime.RemoteObject{}, err } - return CastToReference(out) + return out, nil } -func (rt *Runtime) ReadProperty( - ctx context.Context, - objectID runtime.RemoteObjectID, - propName string, -) (core.Value, error) { - res, err := rt.client.Runtime.GetProperties( - ctx, - runtime.NewGetPropertiesArgs(objectID), - ) +func (rt *Runtime) EvalValue(ctx context.Context, fn *Function) (core.Value, error) { + out, err := rt.call(ctx, fn.returnValue()) if err != nil { return values.None, err } - if res.ExceptionDetails != nil { - return values.None, res.ExceptionDetails + return rt.resolver.ToValue(ctx, out) +} + +func (rt *Runtime) EvalElement(ctx context.Context, fn *Function) (drivers.HTMLElement, error) { + ref, err := rt.EvalRef(ctx, fn) + + if err != nil { + return nil, err } - // all props - if propName == "" { - arr := values.NewArray(len(res.Result)) + return rt.resolver.ToElement(ctx, ref) +} - for _, prop := range res.Result { - if prop.Value != nil { - val, err := Unmarshal(*prop.Value) +func (rt *Runtime) EvalElements(ctx context.Context, fn *Function) (*values.Array, error) { + ref, err := rt.EvalRef(ctx, fn) - if err != nil { - return values.None, err - } + if err != nil { + return nil, err + } - arr.Push(val) - } - } + val, err := rt.resolver.ToValue(ctx, ref) - return arr, nil + if err != nil { + return nil, err } - for _, prop := range res.Result { - if prop.Name == propName { - if prop.Value != nil { - return Unmarshal(*prop.Value) - } + arr, ok := val.(*values.Array) - return values.None, nil - } + if ok { + return arr, nil } - return values.None, nil + return values.NewArrayWith(val), nil } -func (rt *Runtime) call(ctx context.Context, fn *Function) (interface{}, error) { +func (rt *Runtime) call(ctx context.Context, fn *Function) (runtime.RemoteObject, error) { log := rt.logger.With(). Str("expression", fn.String()). Str("returns", fn.returnType.String()). @@ -138,13 +138,13 @@ func (rt *Runtime) call(ctx context.Context, fn *Function) (interface{}, error) if err != nil { log.Trace().Err(err).Msg("failed executing expression") - return nil, errors.Wrap(err, "runtime call") + return runtime.RemoteObject{}, errors.Wrap(err, "runtime call") } if err := parseRuntimeException(repl.ExceptionDetails); err != nil { log.Trace().Err(err).Msg("expression has failed with runtime exception") - return nil, err + return runtime.RemoteObject{}, err } var className string @@ -166,12 +166,5 @@ func (rt *Runtime) call(ctx context.Context, fn *Function) (interface{}, error) Str("return-value", string(repl.Result.Value)). Msg("succeeded executing expression") - switch fn.returnType { - case ReturnValue: - return Unmarshal(repl.Result) - case ReturnRef: - return repl.Result, nil - default: - return nil, nil - } + return repl.Result, nil } diff --git a/pkg/drivers/cdp/eval/types.go b/pkg/drivers/cdp/eval/types.go new file mode 100644 index 00000000..b851b25a --- /dev/null +++ b/pkg/drivers/cdp/eval/types.go @@ -0,0 +1,40 @@ +package eval + +import ( + "github.com/mafredri/cdp/protocol/runtime" +) + +type RemoteType string + +// List of possible remote types +const ( + UnknownType RemoteType = "" + NullType RemoteType = "null" + UndefinedType RemoteType = "undefined" + ArrayType RemoteType = "array" + NodeType RemoteType = "node" + RegexpType RemoteType = "regexp" + DateType RemoteType = "date" + MapType RemoteType = "map" + SetType RemoteType = "set" + WeakMapType RemoteType = "weakmap" + WeakSetType RemoteType = "weakset" + IteratorType RemoteType = "iterator" + GeneratorType RemoteType = "generator" + ErrorType RemoteType = "error" + ProxyType RemoteType = "proxy" + PromiseType RemoteType = "promise" + TypedArrayType RemoteType = "typedarray" + ArrayBufferType RemoteType = "arraybuffer" + DataViewType RemoteType = "dataview" +) + +func ToRemoteType(ref runtime.RemoteObject) RemoteType { + var subtype string + + if ref.Subtype != nil { + subtype = *ref.Subtype + } + + return RemoteType(subtype) +} diff --git a/pkg/drivers/cdp/input/manager.go b/pkg/drivers/cdp/input/manager.go index 2d744850..69e374bf 100644 --- a/pkg/drivers/cdp/input/manager.go +++ b/pkg/drivers/cdp/input/manager.go @@ -2,18 +2,18 @@ package input import ( "context" - "github.com/MontFerret/ferret/pkg/runtime/logging" - "github.com/rs/zerolog" "time" "github.com/mafredri/cdp" "github.com/mafredri/cdp/protocol/dom" "github.com/mafredri/cdp/protocol/runtime" + "github.com/rs/zerolog" "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" "github.com/MontFerret/ferret/pkg/drivers/cdp/templates" "github.com/MontFerret/ferret/pkg/runtime/core" + "github.com/MontFerret/ferret/pkg/runtime/logging" "github.com/MontFerret/ferret/pkg/runtime/values" ) @@ -114,7 +114,7 @@ func (m *Manager) ScrollIntoView(ctx context.Context, id runtime.RemoteObjectID, return nil } -func (m *Manager) ScrollIntoViewBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String, options drivers.ScrollOptions) error { +func (m *Manager) ScrollIntoViewBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector, options drivers.ScrollOptions) error { m.logger.Trace(). Str("selector", selector.String()). Str("behavior", options.Behavior.String()). @@ -179,7 +179,7 @@ func (m *Manager) Focus(ctx context.Context, objectID runtime.RemoteObjectID) er return nil } -func (m *Manager) FocusBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String) error { +func (m *Manager) FocusBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector) error { m.logger.Trace(). Str("parent_object_id", string(id)). Str("selector", selector.String()). @@ -246,7 +246,7 @@ func (m *Manager) Blur(ctx context.Context, objectID runtime.RemoteObjectID) err return nil } -func (m *Manager) BlurBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String) error { +func (m *Manager) BlurBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector) error { m.logger.Trace(). Str("parent_object_id", string(id)). Str("selector", selector.String()). @@ -299,7 +299,7 @@ func (m *Manager) MoveMouse(ctx context.Context, objectID runtime.RemoteObjectID return nil } -func (m *Manager) MoveMouseBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String) error { +func (m *Manager) MoveMouseBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector) error { m.logger.Trace(). Str("parent_object_id", string(id)). Str("selector", selector.String()). @@ -419,12 +419,12 @@ func (m *Manager) Click(ctx context.Context, objectID runtime.RemoteObjectID, co return nil } -func (m *Manager) ClickBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String, count values.Int) error { +func (m *Manager) ClickBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector, count values.Int) error { m.logger.Trace(). Str("parent_object_id", string(id)). - Str("selector", string(selector)). - Int("count", int(count)). - Msg("starting to click on an element by selector") + Str("selector", selector.String()). + Int64("count", int64(count)). + Msg("clicking on an element by selector") if err := m.ScrollIntoViewBySelector(ctx, id, selector, drivers.ScrollOptions{ Behavior: drivers.ScrollBehaviorAuto, @@ -476,75 +476,6 @@ func (m *Manager) ClickBySelector(ctx context.Context, id runtime.RemoteObjectID return nil } -func (m *Manager) ClickBySelectorAll(_ context.Context, _ runtime.RemoteObjectID, _ values.String, _ values.Int) error { - // TODO: Use dom.QueryManager - //m.logger.Trace(). - // Str("parent_object_id", string(id)). - // Str("selector", string(selector)). - // Int("count", int(count)). - // Msg("starting to click on elements by selector") - // - //if err := m.ScrollIntoViewBySelector(ctx, id, selector, drivers.ScrollOptions{ - // Behavior: drivers.ScrollBehaviorAuto, - // Block: drivers.ScrollVerticalAlignmentCenter, - // Inline: drivers.ScrollHorizontalAlignmentCenter, - //}); err != nil { - // return err - //} - // - //m.logger.Trace().Msg("looking up for elements by selector") - // - //found, err := m.exec.EvalRef(ctx, templates.QuerySelectorAll(id, selector)) - // - //if err != nil { - // m.logger.Trace().Err(err).Msg("failed to find an element by selector") - // - // return err - //} - // - //if found.ObjectID == nil { - // m.logger.Trace(). - // Err(core.ErrNotFound). - // Msg("element not found by selector") - // - // return core.ErrNotFound - //} - // - //for idx, nodeID := range found.NodeIDs { - // if idx > 0 { - // m.logger.Trace().Msg("pausing") - // beforeClickDelay := time.Duration(core.NumberLowerBoundary(drivers.DefaultMouseDelay*10)) * time.Millisecond - // - // time.Sleep(beforeClickDelay) - // } - // - // m.logger.Trace().Int("object_id", int(nodeID)).Msg("calculating clickable element points") - // - // points, err := GetClickablePointByNodeID(ctx, m.client, nodeID) - // - // if err != nil { - // m.logger.Trace().Err(err).Msg("failed calculating clickable element points") - // - // return err - // } - // - // m.logger.Trace().Float64("x", points.X).Float64("y", points.Y).Msg("calculated clickable element points") - // - // delay := time.Duration(drivers.DefaultMouseDelay) * time.Millisecond - // - // if err := m.mouse.ClickWithCount(ctx, points.X, points.Y, delay, count); err != nil { - // m.logger.Trace().Err(err).Msg("failed to click on an element") - // return nil - // } - // - // m.logger.Trace().Msg("clicked on an element") - //} - // - //m.logger.Trace().Msg("clicked on all elements") - // - return nil -} - func (m *Manager) Type(ctx context.Context, objectID runtime.RemoteObjectID, params TypeParams) error { m.logger.Trace(). Str("object_id", string(objectID)). @@ -608,10 +539,10 @@ func (m *Manager) Type(ctx context.Context, objectID runtime.RemoteObjectID, par return nil } -func (m *Manager) TypeBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String, params TypeParams) error { +func (m *Manager) TypeBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector, params TypeParams) error { m.logger.Trace(). Str("parent_object_id", string(id)). - Str("selector", string(selector)). + Str("selector", selector.String()). Msg("starting to type text by selector") err := m.ScrollIntoViewBySelector(ctx, id, selector, drivers.ScrollOptions{ @@ -741,10 +672,10 @@ func (m *Manager) Clear(ctx context.Context, objectID runtime.RemoteObjectID) er return nil } -func (m *Manager) ClearBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String) error { +func (m *Manager) ClearBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector) error { m.logger.Trace(). Str("parent_object_id", string(id)). - Str("selector", string(selector)). + Str("selector", selector.String()). Msg("starting to clear element by selector") err := m.ScrollIntoViewBySelector(ctx, id, selector, drivers.ScrollOptions{ @@ -859,10 +790,10 @@ func (m *Manager) Press(ctx context.Context, keys []string, count int) error { return nil } -func (m *Manager) PressBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String, keys []string, count int) error { +func (m *Manager) PressBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector, keys []string, count int) error { m.logger.Trace(). Str("parent_object_id", string(id)). - Str("selector", string(selector)). + Str("selector", selector.String()). Strs("keys", keys). Int("count", count). Msg("starting to press keyboard keys by selector") @@ -909,10 +840,10 @@ func (m *Manager) Select(ctx context.Context, id runtime.RemoteObjectID, value * return arr, nil } -func (m *Manager) SelectBySelector(ctx context.Context, id runtime.RemoteObjectID, selector values.String, value *values.Array) (*values.Array, error) { +func (m *Manager) SelectBySelector(ctx context.Context, id runtime.RemoteObjectID, selector drivers.QuerySelector, value *values.Array) (*values.Array, error) { m.logger.Trace(). Str("parent_object_id", string(id)). - Str("selector", string(selector)). + Str("selector", selector.String()). Msg("starting to select values by selector") if err := m.FocusBySelector(ctx, id, selector); err != nil { diff --git a/pkg/drivers/cdp/input/quad.go b/pkg/drivers/cdp/input/quad.go index 7f4fe17b..176802f3 100644 --- a/pkg/drivers/cdp/input/quad.go +++ b/pkg/drivers/cdp/input/quad.go @@ -111,43 +111,6 @@ func getClickablePoint(ctx context.Context, client *cdp.Client, qargs *dom.GetCo }, nil } -func getClickablePoint2(ctx context.Context, client *cdp.Client, qargs *dom.GetContentQuadsArgs) (Quad, error) { - contentQuadsReply, err := client.DOM.GetContentQuads(ctx, qargs) - - if err != nil { - return Quad{}, err - } - - if contentQuadsReply.Quads == nil || len(contentQuadsReply.Quads) == 0 { - return Quad{}, errors.New("node is either not visible or not an HTMLElement") - } - - content := contentQuadsReply.Quads[0] - - c := len(content) - - if c%2 != 0 || c < 1 { - return Quad{}, errors.New("node is either not visible or not an HTMLElement") - } - - var x, y float64 - for i := 0; i < c; i += 2 { - x += content[i] - y += content[i+1] - } - x /= float64(c / 2) - y /= float64(c / 2) - - return Quad{ - X: x, - Y: y, - }, nil -} - -func GetClickablePointByNodeID(ctx context.Context, client *cdp.Client, nodeID dom.NodeID) (Quad, error) { - return getClickablePoint(ctx, client, dom.NewGetContentQuadsArgs().SetNodeID(nodeID)) -} - func GetClickablePointByObjectID(ctx context.Context, client *cdp.Client, objectID runtime.RemoteObjectID) (Quad, error) { return getClickablePoint(ctx, client, dom.NewGetContentQuadsArgs().SetObjectID(objectID)) } diff --git a/pkg/drivers/cdp/network/manager.go b/pkg/drivers/cdp/network/manager.go index cd02fcc8..5c129fed 100644 --- a/pkg/drivers/cdp/network/manager.go +++ b/pkg/drivers/cdp/network/manager.go @@ -605,7 +605,7 @@ func (m *Manager) WaitForFrameNavigation(ctx context.Context, frameID page.Frame if ctx.Err() == nil { log.Trace().Msg("creating frame execution context") - ec, err := eval.New(ctx, m.logger, m.client, repl.Frame.ID) + ec, err := eval.Create(ctx, m.logger, m.client, repl.Frame.ID) if err != nil { log.Trace().Err(err).Msg("failed to create frame execution context") diff --git a/pkg/drivers/cdp/templates/blur.go b/pkg/drivers/cdp/templates/blur.go index 670eb6f7..07fe4a47 100644 --- a/pkg/drivers/cdp/templates/blur.go +++ b/pkg/drivers/cdp/templates/blur.go @@ -2,9 +2,9 @@ package templates import ( "fmt" + "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" - "github.com/MontFerret/ferret/pkg/runtime/values" "github.com/mafredri/cdp/protocol/runtime" ) @@ -16,18 +16,36 @@ func Blur(id runtime.RemoteObjectID) *eval.Function { return eval.F(blur).WithArgRef(id) } -var blurBySelector = fmt.Sprintf(` +var ( + blurByCSSSelector = fmt.Sprintf(` (el, selector) => { const found = el.querySelector(selector); - if (found == null) { - throw new Error(%s) - } + %s + + found.blur(); + } +`, notFoundErrorFragment) + + blurByXPathSelector = fmt.Sprintf(` + (el, selector) => { + %s + + %s found.blur(); } -`, ParamErr(drivers.ErrNotFound)) +`, xpathAsElementFragment, notFoundErrorFragment) +) + +func BlurBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + var f *eval.Function + + if selector.Kind() == drivers.CSSSelector { + f = eval.F(blurByCSSSelector) + } else { + f = eval.F(blurByXPathSelector) + } -func BlurBySelector(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(blurBySelector).WithArgRef(id).WithArgValue(selector) + return f.WithArgRef(id).WithArgSelector(selector) } diff --git a/pkg/drivers/cdp/templates/global.go b/pkg/drivers/cdp/templates/document.go similarity index 53% rename from pkg/drivers/cdp/templates/global.go rename to pkg/drivers/cdp/templates/document.go index c33cb032..c437e7e8 100644 --- a/pkg/drivers/cdp/templates/global.go +++ b/pkg/drivers/cdp/templates/document.go @@ -13,3 +13,15 @@ return null; func DOMReady() *eval.Function { return eval.F(domReady) } + +const getTitle = `() => document.title` + +func GetTitle() *eval.Function { + return eval.F(getTitle) +} + +const getDocument = `() => document` + +func GetDocument() *eval.Function { + return eval.F(getDocument) +} diff --git a/pkg/drivers/cdp/templates/helpers.go b/pkg/drivers/cdp/templates/helpers.go index e1193f0e..d1ef8284 100644 --- a/pkg/drivers/cdp/templates/helpers.go +++ b/pkg/drivers/cdp/templates/helpers.go @@ -1,86 +1,31 @@ package templates import ( - "bytes" + "fmt" "github.com/MontFerret/ferret/pkg/drivers" - "strconv" - - "github.com/MontFerret/ferret/pkg/runtime/core" - "github.com/MontFerret/ferret/pkg/runtime/values" + "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" ) -func Param(input core.Value) string { - switch value := input.(type) { - case values.String: - return ParamString(value) - case values.Float: - return ParamFloat(value) - case values.Int: - return ParamInt(value) - default: - if value != values.None { - return value.String() - } - - return "null" - } -} - -func ParamList(value []core.Value) string { - var buf bytes.Buffer - lastIndex := len(value) - 1 - - for i, input := range value { - switch v := input.(type) { - case values.String: - buf.WriteString(EscapeString(string(v))) - default: - buf.WriteString(v.String()) - } - - if i != lastIndex { - buf.WriteString(",") - } - } - - return buf.String() -} - -func ParamStringList(value []values.String) string { - var buf bytes.Buffer - lastIndex := len(value) - 1 - - for i, input := range value { - buf.WriteString(EscapeString(string(input))) - - if i != lastIndex { - buf.WriteString(",") +var ( + notFoundErrorFragment = fmt.Sprintf(` + if (found == null) { + throw new Error(%s); } - } - - return buf.String() -} - -func ParamString(value values.String) string { - return EscapeString(string(value)) -} +`, ParamErr(drivers.ErrNotFound)) +) func ParamErr(err error) string { return EscapeString(err.Error()) } -func ParamFloat(value values.Float) string { - return strconv.FormatFloat(float64(value), 'f', 6, 64) -} - -func ParamInt(value values.Int) string { - return strconv.Itoa(int(value)) -} - func EscapeString(value string) string { return "`" + value + "`" } -func flipWhen(when drivers.WaitEvent) drivers.WaitEvent { - return drivers.WaitEvent((int(when) + 1) % 2) +func toFunction(selector drivers.QuerySelector, cssTmpl, xPathTmpl string) *eval.Function { + if selector.Kind() == drivers.CSSSelector { + return eval.F(cssTmpl) + } + + return eval.F(xPathTmpl) } diff --git a/pkg/drivers/cdp/templates/inner_html.go b/pkg/drivers/cdp/templates/inner_html.go index 702bfd55..f7586682 100644 --- a/pkg/drivers/cdp/templates/inner_html.go +++ b/pkg/drivers/cdp/templates/inner_html.go @@ -28,40 +28,71 @@ func GetInnerHTML(id runtime.RemoteObjectID) *eval.Function { return eval.F(getInnerHTML).WithArgRef(id) } -var setInnerHTMLBySelector = fmt.Sprintf(`(el, selector, value) => { +var ( + setInnerHTMLByCSSSelector = fmt.Sprintf(`(el, selector, value) => { const found = el.querySelector(selector); - if (found == null) { - throw new Error(%s); - } + %s + + found.innerHTML = value; +}`, notFoundErrorFragment) + + setInnerHTMLByXPathSelector = fmt.Sprintf(`(el, selector, value) => { + %s + + %s found.innerHTML = value; -}`, ParamErr(drivers.ErrNotFound)) +}`, xpathAsElementFragment, notFoundErrorFragment) +) -func SetInnerHTMLBySelector(id runtime.RemoteObjectID, selector, value values.String) *eval.Function { - return eval.F(setInnerHTMLBySelector).WithArgRef(id).WithArgValue(selector).WithArgValue(value) +func SetInnerHTMLBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector, value values.String) *eval.Function { + return toFunction(selector, setInnerHTMLByCSSSelector, setInnerHTMLByXPathSelector). + WithArgRef(id). + WithArgSelector(selector). + WithArgValue(value) } -var getInnerHTMLBySelector = fmt.Sprintf(`(el, selector) => { +var ( + getInnerHTMLByCSSSelector = fmt.Sprintf(`(el, selector) => { const found = el.querySelector(selector); - if (found == null) { - throw new Error(%s); - } + %s return found.innerHTML; -}`, ParamErr(drivers.ErrNotFound)) +}`, notFoundErrorFragment) + + getInnerHTMLByXPathSelector = fmt.Sprintf(`(el, selector) => { + %s -func GetInnerHTMLBySelector(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(getInnerHTMLBySelector).WithArgRef(id).WithArgValue(selector) + %s + + return found.innerHTML; +}`, xpathAsElementFragment, notFoundErrorFragment) +) + +func GetInnerHTMLBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, getInnerHTMLByCSSSelector, getInnerHTMLByXPathSelector). + WithArgRef(id). + WithArgSelector(selector) } -const getInnerHTMLBySelectorAll = `(el, selector) => { +const getInnerHTMLByCSSSelectorAll = `(el, selector) => { const found = el.querySelectorAll(selector); return Array.from(found).map(i => i.innerHTML); }` -func GetInnerHTMLBySelectorAll(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(getInnerHTMLBySelectorAll).WithArgRef(id).WithArgValue(selector) +var getInnerHTMLByXPathSelectorAll = fmt.Sprintf(`(el, selector) => { + %s + + %s + + return found.map(i => i.innerHTML); +}`, xpathAsElementArrayFragment, notFoundErrorFragment) + +func GetInnerHTMLBySelectorAll(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, getInnerHTMLByCSSSelectorAll, getInnerHTMLByXPathSelectorAll). + WithArgRef(id). + WithArgSelector(selector) } diff --git a/pkg/drivers/cdp/templates/inner_text.go b/pkg/drivers/cdp/templates/inner_text.go index b591720f..09a80052 100644 --- a/pkg/drivers/cdp/templates/inner_text.go +++ b/pkg/drivers/cdp/templates/inner_text.go @@ -28,47 +28,81 @@ func GetInnerText(id runtime.RemoteObjectID) *eval.Function { return eval.F(getInnerText).WithArgRef(id) } -var setInnerTextBySelector = fmt.Sprintf(` +var ( + setInnerTextByCSSSelector = fmt.Sprintf(` (el, selector, value) => { const found = el.querySelector(selector); - if (found == null) { - throw new Error(%s); - } + %s + + found.innerText = value; +}`, notFoundErrorFragment) + + setInnerTextByXPathSelector = fmt.Sprintf(` +(el, selector, value) => { + %s + + %s found.innerText = value; -}`, ParamErr(drivers.ErrNotFound)) +}`, xpathAsElementFragment, notFoundErrorFragment) +) -func SetInnerTextBySelector(id runtime.RemoteObjectID, selector, value values.String) *eval.Function { - return eval.F(setInnerTextBySelector).WithArgRef(id).WithArgValue(selector).WithArgValue(value) +func SetInnerTextBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector, value values.String) *eval.Function { + return toFunction(selector, setInnerTextByCSSSelector, setInnerTextByXPathSelector). + WithArgRef(id). + WithArgSelector(selector). + WithArgValue(value) } -var getInnerTextBySelector = fmt.Sprintf(` +var ( + getInnerTextByCSSSelector = fmt.Sprintf(` (el, selector) => { const found = el.querySelector(selector); - if (found == null) { - throw new Error(%s); - } + %s + + return found.innerText; +}`, notFoundErrorFragment) + + getInnerTextByXPathSelector = fmt.Sprintf(` +(el, selector) => { + %s + + %s return found.innerText; -}`, ParamErr(drivers.ErrNotFound)) +}`, xpathAsElementFragment, notFoundErrorFragment) +) -func GetInnerTextBySelector(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(getInnerTextBySelector).WithArgRef(id).WithArgValue(selector) +func GetInnerTextBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, getInnerTextByCSSSelector, getInnerTextByXPathSelector). + WithArgRef(id). + WithArgSelector(selector) } -var getInnerTextBySelectorAll = fmt.Sprintf(` +var ( + getInnerTextByCSSSelectorAll = fmt.Sprintf(` (el, selector) => { const found = el.querySelectorAll(selector); - if (found == null) { - throw new Error(%s); - } + %s return Array.from(found).map(i => i.innerText); -}`, ParamErr(drivers.ErrNotFound)) +}`, notFoundErrorFragment) + + getInnerTextByXPathSelectorAll = fmt.Sprintf(` +(el, selector) => { + %s + + %s + + return found.map(i => i.innerText); +}`, xpathAsElementArrayFragment, notFoundErrorFragment) +) -func GetInnerTextBySelectorAll(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(getInnerTextBySelectorAll).WithArgRef(id).WithArgValue(selector) +func GetInnerTextBySelectorAll(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, getInnerTextByCSSSelectorAll, getInnerTextByXPathSelectorAll). + WithArgRef(id). + WithArgSelector(selector) } diff --git a/pkg/drivers/cdp/templates/query.go b/pkg/drivers/cdp/templates/query.go index 533d7072..382f4413 100644 --- a/pkg/drivers/cdp/templates/query.go +++ b/pkg/drivers/cdp/templates/query.go @@ -4,37 +4,63 @@ import ( "fmt" "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" - "github.com/MontFerret/ferret/pkg/runtime/values" "github.com/mafredri/cdp/protocol/runtime" ) -var querySelector = fmt.Sprintf(` +const ( + queryCSSSelectorFragment = "const found = el.querySelector(selector);" + + queryCSSSelectorAllFragment = "const found = el.querySelectorAll(selector);" +) + +var ( + queryCSSSelector = fmt.Sprintf(` (el, selector) => { const found = el.querySelector(selector); - if (found == null) { - throw new Error(%s); - } + %s return found; } `, - ParamErr(drivers.ErrNotFound), + notFoundErrorFragment, + ) + queryXPathSelector = fmt.Sprintf(` + (el, selector) => { + %s + + %s + + return found; + } + `, + xpathAsElementFragment, notFoundErrorFragment, + ) ) -func QuerySelector(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(querySelector).WithArgRef(id).WithArgValue(selector) +func QuerySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, queryCSSSelector, queryXPathSelector). + WithArgRef(id). + WithArgSelector(selector) } -const querySelectorAll = `(el, selector) => { +const queryCSSSelectorAll = `(el, selector) => { return el.querySelectorAll(selector); }` -func QuerySelectorAll(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(querySelectorAll).WithArgRef(id).WithArgValue(selector) +var queryXPathSelectorAll = fmt.Sprintf(`(el, selector) => { + %s + + return found; +}`, xpathAsElementArrayFragment) + +func QuerySelectorAll(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, queryCSSSelectorAll, queryXPathSelectorAll). + WithArgRef(id). + WithArgSelector(selector) } -const existsBySelector = ` +const existsByCSSSelector = ` (el, selector) => { const found = el.querySelector(selector); @@ -42,11 +68,21 @@ const existsBySelector = ` } ` -func ExistsBySelector(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(existsBySelector).WithArgRef(id).WithArgValue(selector) +var existsByXPathSelector = fmt.Sprintf(` + (el, selector) => { + %s + + return found != null; + } +`, xpathAsElementFragment) + +func ExistsBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, existsByCSSSelector, existsByXPathSelector). + WithArgRef(id). + WithArgSelector(selector) } -const countBySelector = ` +const countByCSSSelector = ` (el, selector) => { const found = el.querySelectorAll(selector); @@ -54,6 +90,16 @@ const countBySelector = ` } ` -func CountBySelector(id runtime.RemoteObjectID, selector values.String) *eval.Function { - return eval.F(countBySelector).WithArgRef(id).WithArgValue(selector) +var countByXPathSelector = fmt.Sprintf(` + (el, selector) => { + %s + + return found.length; + } +`, xpathAsElementArrayFragment) + +func CountBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector) *eval.Function { + return toFunction(selector, countByCSSSelector, countByXPathSelector). + WithArgRef(id). + WithArgSelector(selector) } diff --git a/pkg/drivers/cdp/templates/scroll.go b/pkg/drivers/cdp/templates/scroll.go index d3be7582..fe2b8edc 100644 --- a/pkg/drivers/cdp/templates/scroll.go +++ b/pkg/drivers/cdp/templates/scroll.go @@ -2,11 +2,11 @@ package templates import ( "fmt" + + "github.com/mafredri/cdp/protocol/runtime" + "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" - "github.com/MontFerret/ferret/pkg/runtime/core" - "github.com/MontFerret/ferret/pkg/runtime/values" - "github.com/mafredri/cdp/protocol/runtime" ) const ( @@ -67,17 +67,33 @@ var ( return true; }`, isElementInViewportFragment) - scrollIntoViewBySelector = fmt.Sprintf(`(parent, selector, opts) => { - const el = parent.querySelector(selector); + scrollIntoViewByCSSSelector = fmt.Sprintf(`(el, selector, opts) => { + const found = el.querySelector(selector); - if (el == null) { - throw new Error(%s); + %s + + %s + + if (!isInViewport(found)) { + found.scrollIntoView({ + behavior: opts.behavior, + block: opts.block, + inline: opts.inline + }); } + return true; +}`, notFoundErrorFragment, isElementInViewportFragment) + + scrollIntoViewByXPathSelector = fmt.Sprintf(`(el, selector, opts) => { + %s + + %s + %s - if (!isInViewport(el)) { - el.scrollIntoView({ + if (!isInViewport(found)) { + found.scrollIntoView({ behavior: opts.behavior, block: opts.block, inline: opts.inline @@ -85,7 +101,7 @@ var ( } return true; -}`, ParamErr(core.ErrNotFound), isElementInViewportFragment) +}`, xpathAsElementFragment, notFoundErrorFragment, isElementInViewportFragment) ) func Scroll(options drivers.ScrollOptions) *eval.Function { @@ -104,6 +120,9 @@ func ScrollIntoView(id runtime.RemoteObjectID, options drivers.ScrollOptions) *e return eval.F(scrollIntoView).WithArgRef(id).WithArg(options) } -func ScrollIntoViewBySelector(id runtime.RemoteObjectID, selector values.String, options drivers.ScrollOptions) *eval.Function { - return eval.F(scrollIntoViewBySelector).WithArgRef(id).WithArgValue(selector).WithArg(options) +func ScrollIntoViewBySelector(id runtime.RemoteObjectID, selector drivers.QuerySelector, options drivers.ScrollOptions) *eval.Function { + return toFunction(selector, scrollIntoViewByCSSSelector, scrollIntoViewByXPathSelector). + WithArgRef(id). + WithArgSelector(selector). + WithArg(options) } diff --git a/pkg/drivers/cdp/templates/select.go b/pkg/drivers/cdp/templates/select.go index 2403db66..86a0db51 100644 --- a/pkg/drivers/cdp/templates/select.go +++ b/pkg/drivers/cdp/templates/select.go @@ -2,51 +2,66 @@ package templates import ( "fmt" + "github.com/MontFerret/ferret/pkg/drivers" "github.com/MontFerret/ferret/pkg/drivers/cdp/eval" - "github.com/MontFerret/ferret/pkg/runtime/core" "github.com/MontFerret/ferret/pkg/runtime/values" "github.com/mafredri/cdp/protocol/runtime" ) const selectFragment = ` - if (el.nodeName.toLowerCase() !== 'select') { + if (found.nodeName.toLowerCase() !== 'select') { throw new Error('element is not a