From b85041e682227be760b8c65a9485c6637113416b Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:27:48 +0100 Subject: [PATCH 1/8] add locator expectation --- lib/playwright/locator_impl.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/playwright/locator_impl.rb b/lib/playwright/locator_impl.rb index f5c96c9c..b4eed577 100644 --- a/lib/playwright/locator_impl.rb +++ b/lib/playwright/locator_impl.rb @@ -488,5 +488,22 @@ def all_text_contents def highlight @frame.highlight(@selector) end + + def expect(expression, options) + if options.key? :expected_value + options[:expected_value] = JavaScript::ValueSerializer + .new(options[:expected_value]) + .serialize + end + + @frame.channel.send_message_to_server_result( + "expect", + { + selector: @selector, + expression:, + **options, + } + ) + end end end From 142b9b0c583a1fe02b9c296ab7302be738abab0d Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:08:33 +0100 Subject: [PATCH 2/8] implementation of locator assertions and expectations --- development/generate_api.rb | 2 + development/unimplemented_examples.md | 267 ++++++++ documentation/docs/api/locator_assertions.md | 684 +++++++++++++++++++ documentation/docs/include/api_coverage.md | 43 ++ lib/playwright/expect.rb | 32 + lib/playwright/locator_assertions_impl.rb | 404 +++++++++++ 6 files changed, 1432 insertions(+) create mode 100644 documentation/docs/api/locator_assertions.md create mode 100644 lib/playwright/expect.rb create mode 100644 lib/playwright/locator_assertions_impl.rb diff --git a/development/generate_api.rb b/development/generate_api.rb index 0f1d2864..ae7aee17 100644 --- a/development/generate_api.rb +++ b/development/generate_api.rb @@ -29,6 +29,7 @@ APIResponse APIRequestContext APIRequest + LocatorAssertions ] EXPERIMENTAL = %w[ Android @@ -50,6 +51,7 @@ FrameLocator APIRequest APIResponse + LocatorAssertions ] require 'bundler/setup' diff --git a/development/unimplemented_examples.md b/development/unimplemented_examples.md index 8a4e41d5..980b651c 100644 --- a/development/unimplemented_examples.md +++ b/development/unimplemented_examples.md @@ -450,3 +450,270 @@ locator.press_sequentially("my password") locator.press("Enter") ``` + +### example_eff0600f575bf375d7372280ca8e6dfc51d927ced49fbcb75408c894b9e0564e (LocatorAssertions) + +``` +from playwright.sync_api import Page, expect + +def test_status_becomes_submitted(page: Page) -> None: + # .. + page.get_by_role("button").click() + expect(page.locator(".status")).to_have_text("Submitted") + +``` + +### example_781b6f44dd462fc3753b3e48d6888f2ef4d0794253bf6ffb4c42c76f5ec3b454 (LocatorAssertions#to_be_attached) + +``` +expect(page.get_by_text("Hidden text")).to_be_attached() + +``` + +### example_00a58b66eec12973ab87c0ce5004126aa1f1af5a971a9e89638669f729bbb1b6 (LocatorAssertions#to_be_checked) + +``` +from playwright.sync_api import expect + +locator = page.get_by_label("Subscribe to newsletter") +expect(locator).to_be_checked() + +``` + +### example_fc3052bc38e6c1968f23f9185bda7f06478af4719ce96f6a49878ea7e72c9a82 (LocatorAssertions#to_be_disabled) + +``` +from playwright.sync_api import expect + +locator = page.locator("button.submit") +expect(locator).to_be_disabled() + +``` + +### example_a42b1e97cd0899ccd72bc4b74ab8f57c549814ca5b6d1bb912c870153d6d3f8d (LocatorAssertions#to_be_editable) + +``` +from playwright.sync_api import expect + +locator = page.get_by_role("textbox") +expect(locator).to_be_editable() + +``` + +### example_1fb5a7ee389401cf5a6fb3ba90c5b58c42c93d43aa5e4e34d99a5c6265ce0b35 (LocatorAssertions#to_be_empty) + +``` +from playwright.sync_api import expect + +locator = page.locator("div.warning") +expect(locator).to_be_empty() + +``` + +### example_0389b23d34a430ee418fd2138f9b8269df20fb6595f2618400e3d53b4f344a75 (LocatorAssertions#to_be_enabled) + +``` +from playwright.sync_api import expect + +locator = page.locator("button.submit") +expect(locator).to_be_enabled() + +``` + +### example_9fc7c2560e0a8117bc4ba14d6133a3d9c66cf6461c29c5a74fe132dea8bd8d63 (LocatorAssertions#to_be_focused) + +``` +from playwright.sync_api import expect + +locator = page.get_by_role("textbox") +expect(locator).to_be_focused() + +``` + +### example_55b9181de8eb71936b5e5289631fca33d2100f47f4c4e832d92c23f923779c62 (LocatorAssertions#to_be_hidden) + +``` +from playwright.sync_api import expect + +locator = page.locator('.my-element') +expect(locator).to_be_hidden() + +``` + +### example_7d5d5657528a32a8fb24cbf30e7bb3154cdf4c426e84e40131445a38fe8df2ee (LocatorAssertions#to_be_in_viewport) + +``` +from playwright.sync_api import expect + +locator = page.get_by_role("button") +# Make sure at least some part of element intersects viewport. +expect(locator).to_be_in_viewport() +# Make sure element is fully outside of viewport. +expect(locator).not_to_be_in_viewport() +# Make sure that at least half of the element intersects viewport. +expect(locator).to_be_in_viewport(ratio=0.5) + +``` + +### example_84ccd2ec31f9f00136a2931e9abb9c766eab967a6e892d3dcf90c02f14e5117f (LocatorAssertions#to_be_visible) + +``` +expect(page.get_by_text("Welcome")).to_be_visible() + +``` + +### example_3553a48e2a15853f4869604ef20dae14952c16abfa0570b8f02e9b74e3d84faa (LocatorAssertions#to_contain_text) + +``` +import re +from playwright.sync_api import expect + +locator = page.locator('.title') +expect(locator).to_contain_text("substring") +expect(locator).to_contain_text(re.compile(r"\d messages")) + +``` + +### example_fb3cde55b658aefe2e54f93e5b78d26f25cd376eaa469434631af079bb8d8a62 (LocatorAssertions#to_contain_text) + +``` +from playwright.sync_api import expect + +# ✓ Contains the right items in the right order +expect(page.locator("ul > li")).to_contain_text(["Text 1", "Text 3", "Text 4"]) + +# ✖ Wrong order +expect(page.locator("ul > li")).to_contain_text(["Text 3", "Text 2"]) + +# ✖ No item contains this text +expect(page.locator("ul > li")).to_contain_text(["Some 33"]) + +# ✖ Locator points to the outer list element, not to the list items +expect(page.locator("ul")).to_contain_text(["Text 3"]) + +``` + +### example_709faaa456b4775109b1fbaca74a86ac5107af5e4801ea07cb690942f1d37f88 (LocatorAssertions#to_have_attribute) + +``` +from playwright.sync_api import expect + +locator = page.locator("input") +expect(locator).to_have_attribute("type", "text") + +``` + +### example_c16c6c567ee66b6d60de634c8a8a7c7c2b26f0e9ea8556e50a47d0c151935aa1 (LocatorAssertions#to_have_class) + +``` +from playwright.sync_api import expect + +locator = page.locator("#component") +expect(locator).to_have_class(re.compile(r"selected")) +expect(locator).to_have_class("selected row") + +``` + +### example_96b9affd86317eeafe4a419f6ec484d33cea4ee947297f44b7b4ebb373261f1d (LocatorAssertions#to_have_class) + +``` +from playwright.sync_api import expect + +locator = page.locator("list > .component") +expect(locator).to_have_class(["component", "component selected", "component"]) + +``` + +### example_b3e3d5c7f2ff3a225541e57968953a77e32048daddaabe29ba84e93a1fcee84f (LocatorAssertions#to_have_count) + +``` +from playwright.sync_api import expect + +locator = page.locator("list > .component") +expect(locator).to_have_count(3) + +``` + +### example_12c52b928c1fac117b68573a914ce0ef9595becead95a0ee7c1f487ba1ad9010 (LocatorAssertions#to_have_css) + +``` +from playwright.sync_api import expect + +locator = page.get_by_role("button") +expect(locator).to_have_css("display", "flex") + +``` + +### example_5a4c0b1802f0751c2e1068d831ecd499b36a7860605050ba976c2290452bbd89 (LocatorAssertions#to_have_id) + +``` +from playwright.sync_api import expect + +locator = page.get_by_role("textbox") +expect(locator).to_have_id("lastname") + +``` + +### example_01cad4288f995d4b6253003eb0f4acb227e80553410cea0a8db0ab6927247d92 (LocatorAssertions#to_have_js_property) + +``` +from playwright.sync_api import expect + +locator = page.locator(".component") +expect(locator).to_have_js_property("loaded", True) + +``` + +### example_4ece81163bcb1edeccd7cea8f8c6158cf794c8ef88a673e8c5350a10eaa81542 (LocatorAssertions#to_have_text) + +``` +import re +from playwright.sync_api import expect + +locator = page.locator(".title") +expect(locator).to_have_text(re.compile(r"Welcome, Test User")) +expect(locator).to_have_text(re.compile(r"Welcome, .*")) + +``` + +### example_2caa32069462b536399b1e7e9ade6388ab8b83912ae46ba293cf8ed241c48e85 (LocatorAssertions#to_have_text) + +``` +from playwright.sync_api import expect + +# ✓ Has the right items in the right order +expect(page.locator("ul > li")).to_have_text(["Text 1", "Text 2", "Text 3"]) + +# ✖ Wrong order +expect(page.locator("ul > li")).to_have_text(["Text 3", "Text 2", "Text 1"]) + +# ✖ Last item does not match +expect(page.locator("ul > li")).to_have_text(["Text 1", "Text 2", "Text"]) + +# ✖ Locator points to the outer list element, not to the list items +expect(page.locator("ul")).to_have_text(["Text 1", "Text 2", "Text 3"]) + +``` + +### example_84f23ac0426bebae60693613034771d70a26808dff53d1d476c3f5856346521a (LocatorAssertions#to_have_value) + +``` +import re +from playwright.sync_api import expect + +locator = page.locator("input[type=number]") +expect(locator).to_have_value(re.compile(r"[0-9]")) + +``` + +### example_e5cce4bcdea914bbae14a3645b77f19c322038b0ef81d6ad2a1c9f5b0e21b1e9 (LocatorAssertions#to_have_values) + +``` +import re +from playwright.sync_api import expect + +locator = page.locator("id=favorite-colors") +locator.select_option(["R", "G"]) +expect(locator).to_have_values([re.compile(r"R"), re.compile(r"G")]) + +``` diff --git a/documentation/docs/api/locator_assertions.md b/documentation/docs/api/locator_assertions.md new file mode 100644 index 00000000..da3a61b6 --- /dev/null +++ b/documentation/docs/api/locator_assertions.md @@ -0,0 +1,684 @@ +--- +sidebar_position: 10 +--- + +# LocatorAssertions + + +The [LocatorAssertions](./locator_assertions) class provides assertion methods that can be used to make assertions about the [Locator](./locator) state in the tests. + +```python sync title=example_eff0600f575bf375d7372280ca8e6dfc51d927ced49fbcb75408c894b9e0564e.py +from playwright.sync_api import Page, expect + +def test_status_becomes_submitted(page: Page) -> None: + # .. + page.get_by_role("button").click() + expect(page.locator(".status")).to_have_text("Submitted") + +``` + +## not_to_be_attached + +``` +def not_to_be_attached(attached: nil, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_attached](./locator_assertions#to_be_attached). + +## not_to_be_checked + +``` +def not_to_be_checked(timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_checked](./locator_assertions#to_be_checked). + +## not_to_be_disabled + +``` +def not_to_be_disabled(timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_disabled](./locator_assertions#to_be_disabled). + +## not_to_be_editable + +``` +def not_to_be_editable(editable: nil, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_editable](./locator_assertions#to_be_editable). + +## not_to_be_empty + +``` +def not_to_be_empty(timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_empty](./locator_assertions#to_be_empty). + +## not_to_be_enabled + +``` +def not_to_be_enabled(enabled: nil, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_enabled](./locator_assertions#to_be_enabled). + +## not_to_be_focused + +``` +def not_to_be_focused(timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_focused](./locator_assertions#to_be_focused). + +## not_to_be_hidden + +``` +def not_to_be_hidden(timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_hidden](./locator_assertions#to_be_hidden). + +## not_to_be_in_viewport + +``` +def not_to_be_in_viewport(ratio: nil, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_be_in_viewport](./locator_assertions#to_be_in_viewport). + +## not_to_be_visible + +``` +def not_to_be_visible(timeout: nil, visible: nil) +``` + + +The opposite of [LocatorAssertions#to_be_visible](./locator_assertions#to_be_visible). + +## not_to_contain_text + +``` +def not_to_contain_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) +``` + + +The opposite of [LocatorAssertions#to_contain_text](./locator_assertions#to_contain_text). + +## not_to_have_attribute + +``` +def not_to_have_attribute(name, value, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_attribute](./locator_assertions#to_have_attribute). + +## not_to_have_class + +``` +def not_to_have_class(expected, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_class](./locator_assertions#to_have_class). + +## not_to_have_count + +``` +def not_to_have_count(count, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_count](./locator_assertions#to_have_count). + +## not_to_have_css + +``` +def not_to_have_css(name, value, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_css](./locator_assertions#to_have_css). + +## not_to_have_id + +``` +def not_to_have_id(id, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_id](./locator_assertions#to_have_id). + +## not_to_have_js_property + +``` +def not_to_have_js_property(name, value, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_js_property](./locator_assertions#to_have_js_property). + +## not_to_have_text + +``` +def not_to_have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) +``` + + +The opposite of [LocatorAssertions#to_have_text](./locator_assertions#to_have_text). + +## not_to_have_value + +``` +def not_to_have_value(value, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_value](./locator_assertions#to_have_value). + +## not_to_have_values + +``` +def not_to_have_values(values, timeout: nil) +``` + + +The opposite of [LocatorAssertions#to_have_values](./locator_assertions#to_have_values). + +## to_be_attached + +``` +def to_be_attached(attached: nil, timeout: nil) +``` + + +Ensures that [Locator](./locator) points to an [attached](https://playwright.dev/python/docs/actionability#attached) DOM node. + +**Usage** + +```python sync title=example_781b6f44dd462fc3753b3e48d6888f2ef4d0794253bf6ffb4c42c76f5ec3b454.py +expect(page.get_by_text("Hidden text")).to_be_attached() + +``` + +## to_be_checked + +``` +def to_be_checked(checked: nil, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to a checked input. + +**Usage** + +```python sync title=example_00a58b66eec12973ab87c0ce5004126aa1f1af5a971a9e89638669f729bbb1b6.py +from playwright.sync_api import expect + +locator = page.get_by_label("Subscribe to newsletter") +expect(locator).to_be_checked() + +``` + +## to_be_disabled + +``` +def to_be_disabled(timeout: nil) +``` + + +Ensures the [Locator](./locator) points to a disabled element. Element is disabled if it has "disabled" attribute +or is disabled via ['aria-disabled'](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled). +Note that only native control elements such as HTML `button`, `input`, `select`, `textarea`, `option`, `optgroup` +can be disabled by setting "disabled" attribute. "disabled" attribute on other elements is ignored +by the browser. + +**Usage** + +```python sync title=example_fc3052bc38e6c1968f23f9185bda7f06478af4719ce96f6a49878ea7e72c9a82.py +from playwright.sync_api import expect + +locator = page.locator("button.submit") +expect(locator).to_be_disabled() + +``` + +## to_be_editable + +``` +def to_be_editable(editable: nil, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an editable element. + +**Usage** + +```python sync title=example_a42b1e97cd0899ccd72bc4b74ab8f57c549814ca5b6d1bb912c870153d6d3f8d.py +from playwright.sync_api import expect + +locator = page.get_by_role("textbox") +expect(locator).to_be_editable() + +``` + +## to_be_empty + +``` +def to_be_empty(timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an empty editable element or to a DOM node that has no text. + +**Usage** + +```python sync title=example_1fb5a7ee389401cf5a6fb3ba90c5b58c42c93d43aa5e4e34d99a5c6265ce0b35.py +from playwright.sync_api import expect + +locator = page.locator("div.warning") +expect(locator).to_be_empty() + +``` + +## to_be_enabled + +``` +def to_be_enabled(enabled: nil, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an enabled element. + +**Usage** + +```python sync title=example_0389b23d34a430ee418fd2138f9b8269df20fb6595f2618400e3d53b4f344a75.py +from playwright.sync_api import expect + +locator = page.locator("button.submit") +expect(locator).to_be_enabled() + +``` + +## to_be_focused + +``` +def to_be_focused(timeout: nil) +``` + + +Ensures the [Locator](./locator) points to a focused DOM node. + +**Usage** + +```python sync title=example_9fc7c2560e0a8117bc4ba14d6133a3d9c66cf6461c29c5a74fe132dea8bd8d63.py +from playwright.sync_api import expect + +locator = page.get_by_role("textbox") +expect(locator).to_be_focused() + +``` + +## to_be_hidden + +``` +def to_be_hidden(timeout: nil) +``` + + +Ensures that [Locator](./locator) either does not resolve to any DOM node, or resolves to a [non-visible](https://playwright.dev/python/docs/actionability#visible) one. + +**Usage** + +```python sync title=example_55b9181de8eb71936b5e5289631fca33d2100f47f4c4e832d92c23f923779c62.py +from playwright.sync_api import expect + +locator = page.locator('.my-element') +expect(locator).to_be_hidden() + +``` + +## to_be_in_viewport + +``` +def to_be_in_viewport(ratio: nil, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an element that intersects viewport, according to the [intersection observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). + +**Usage** + +```python sync title=example_7d5d5657528a32a8fb24cbf30e7bb3154cdf4c426e84e40131445a38fe8df2ee.py +from playwright.sync_api import expect + +locator = page.get_by_role("button") +# Make sure at least some part of element intersects viewport. +expect(locator).to_be_in_viewport() +# Make sure element is fully outside of viewport. +expect(locator).not_to_be_in_viewport() +# Make sure that at least half of the element intersects viewport. +expect(locator).to_be_in_viewport(ratio=0.5) + +``` + +## to_be_visible + +``` +def to_be_visible(timeout: nil, visible: nil) +``` + + +Ensures that [Locator](./locator) points to an [attached](https://playwright.dev/python/docs/actionability#attached) and [visible](https://playwright.dev/python/docs/actionability#visible) DOM node. + +**Usage** + +```python sync title=example_84ccd2ec31f9f00136a2931e9abb9c766eab967a6e892d3dcf90c02f14e5117f.py +expect(page.get_by_text("Welcome")).to_be_visible() + +``` + +## to_contain_text + +``` +def to_contain_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) +``` + + +Ensures the [Locator](./locator) points to an element that contains the given text. You can use regular expressions for the value as well. + +**Usage** + +```python sync title=example_3553a48e2a15853f4869604ef20dae14952c16abfa0570b8f02e9b74e3d84faa.py +import re +from playwright.sync_api import expect + +locator = page.locator('.title') +expect(locator).to_contain_text("substring") +expect(locator).to_contain_text(re.compile(r"\d messages")) + +``` + +If you pass an array as an expected value, the expectations are: +1. Locator resolves to a list of elements. +1. Elements from a **subset** of this list contain text from the expected array, respectively. +1. The matching subset of elements has the same order as the expected array. +1. Each text value from the expected array is matched by some element from the list. + +For example, consider the following list: + +```html + +``` + +Let's see how we can use the assertion: + +```python sync title=example_fb3cde55b658aefe2e54f93e5b78d26f25cd376eaa469434631af079bb8d8a62.py +from playwright.sync_api import expect + +# ✓ Contains the right items in the right order +expect(page.locator("ul > li")).to_contain_text(["Text 1", "Text 3", "Text 4"]) + +# ✖ Wrong order +expect(page.locator("ul > li")).to_contain_text(["Text 3", "Text 2"]) + +# ✖ No item contains this text +expect(page.locator("ul > li")).to_contain_text(["Some 33"]) + +# ✖ Locator points to the outer list element, not to the list items +expect(page.locator("ul")).to_contain_text(["Text 3"]) + +``` + +## to_have_attribute + +``` +def to_have_attribute(name, value, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an element with given attribute. + +**Usage** + +```python sync title=example_709faaa456b4775109b1fbaca74a86ac5107af5e4801ea07cb690942f1d37f88.py +from playwright.sync_api import expect + +locator = page.locator("input") +expect(locator).to_have_attribute("type", "text") + +``` + +## to_have_class + +``` +def to_have_class(expected, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an element with given CSS classes. This needs to be a full match +or using a relaxed regular expression. + +**Usage** + +```html +
+``` + +```python sync title=example_c16c6c567ee66b6d60de634c8a8a7c7c2b26f0e9ea8556e50a47d0c151935aa1.py +from playwright.sync_api import expect + +locator = page.locator("#component") +expect(locator).to_have_class(re.compile(r"selected")) +expect(locator).to_have_class("selected row") + +``` + +Note that if array is passed as an expected value, entire lists of elements can be asserted: + +```python sync title=example_96b9affd86317eeafe4a419f6ec484d33cea4ee947297f44b7b4ebb373261f1d.py +from playwright.sync_api import expect + +locator = page.locator("list > .component") +expect(locator).to_have_class(["component", "component selected", "component"]) + +``` + +## to_have_count + +``` +def to_have_count(count, timeout: nil) +``` + + +Ensures the [Locator](./locator) resolves to an exact number of DOM nodes. + +**Usage** + +```python sync title=example_b3e3d5c7f2ff3a225541e57968953a77e32048daddaabe29ba84e93a1fcee84f.py +from playwright.sync_api import expect + +locator = page.locator("list > .component") +expect(locator).to_have_count(3) + +``` + +## to_have_css + +``` +def to_have_css(name, value, timeout: nil) +``` + + +Ensures the [Locator](./locator) resolves to an element with the given computed CSS style. + +**Usage** + +```python sync title=example_12c52b928c1fac117b68573a914ce0ef9595becead95a0ee7c1f487ba1ad9010.py +from playwright.sync_api import expect + +locator = page.get_by_role("button") +expect(locator).to_have_css("display", "flex") + +``` + +## to_have_id + +``` +def to_have_id(id, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an element with the given DOM Node ID. + +**Usage** + +```python sync title=example_5a4c0b1802f0751c2e1068d831ecd499b36a7860605050ba976c2290452bbd89.py +from playwright.sync_api import expect + +locator = page.get_by_role("textbox") +expect(locator).to_have_id("lastname") + +``` + +## to_have_js_property + +``` +def to_have_js_property(name, value, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an element with given JavaScript property. Note that this property can be +of a primitive type as well as a plain serializable JavaScript object. + +**Usage** + +```python sync title=example_01cad4288f995d4b6253003eb0f4acb227e80553410cea0a8db0ab6927247d92.py +from playwright.sync_api import expect + +locator = page.locator(".component") +expect(locator).to_have_js_property("loaded", True) + +``` + +## to_have_text + +``` +def to_have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) +``` + + +Ensures the [Locator](./locator) points to an element with the given text. You can use regular expressions for the value as well. + +**Usage** + +```python sync title=example_4ece81163bcb1edeccd7cea8f8c6158cf794c8ef88a673e8c5350a10eaa81542.py +import re +from playwright.sync_api import expect + +locator = page.locator(".title") +expect(locator).to_have_text(re.compile(r"Welcome, Test User")) +expect(locator).to_have_text(re.compile(r"Welcome, .*")) + +``` + +If you pass an array as an expected value, the expectations are: +1. Locator resolves to a list of elements. +1. The number of elements equals the number of expected values in the array. +1. Elements from the list have text matching expected array values, one by one, in order. + +For example, consider the following list: + +```html + +``` + +Let's see how we can use the assertion: + +```python sync title=example_2caa32069462b536399b1e7e9ade6388ab8b83912ae46ba293cf8ed241c48e85.py +from playwright.sync_api import expect + +# ✓ Has the right items in the right order +expect(page.locator("ul > li")).to_have_text(["Text 1", "Text 2", "Text 3"]) + +# ✖ Wrong order +expect(page.locator("ul > li")).to_have_text(["Text 3", "Text 2", "Text 1"]) + +# ✖ Last item does not match +expect(page.locator("ul > li")).to_have_text(["Text 1", "Text 2", "Text"]) + +# ✖ Locator points to the outer list element, not to the list items +expect(page.locator("ul")).to_have_text(["Text 1", "Text 2", "Text 3"]) + +``` + +## to_have_value + +``` +def to_have_value(value, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to an element with the given input value. You can use regular expressions for the value as well. + +**Usage** + +```python sync title=example_84f23ac0426bebae60693613034771d70a26808dff53d1d476c3f5856346521a.py +import re +from playwright.sync_api import expect + +locator = page.locator("input[type=number]") +expect(locator).to_have_value(re.compile(r"[0-9]")) + +``` + +## to_have_values + +``` +def to_have_values(values, timeout: nil) +``` + + +Ensures the [Locator](./locator) points to multi-select/combobox (i.e. a `select` with the `multiple` attribute) and the specified values are selected. + +**Usage** + +For example, given the following element: + +```html + +``` + +```python sync title=example_e5cce4bcdea914bbae14a3645b77f19c322038b0ef81d6ad2a1c9f5b0e21b1e9.py +import re +from playwright.sync_api import expect + +locator = page.locator("id=favorite-colors") +locator.select_option(["R", "G"]) +expect(locator).to_have_values([re.compile(r"R"), re.compile(r"G")]) + +``` diff --git a/documentation/docs/include/api_coverage.md b/documentation/docs/include/api_coverage.md index fda77ee1..35745268 100644 --- a/documentation/docs/include/api_coverage.md +++ b/documentation/docs/include/api_coverage.md @@ -527,6 +527,49 @@ * ~~new_context~~ +## LocatorAssertions + +* not_to_be_attached +* not_to_be_checked +* not_to_be_disabled +* not_to_be_editable +* not_to_be_empty +* not_to_be_enabled +* not_to_be_focused +* not_to_be_hidden +* not_to_be_in_viewport +* not_to_be_visible +* not_to_contain_text +* not_to_have_attribute +* not_to_have_class +* not_to_have_count +* not_to_have_css +* not_to_have_id +* not_to_have_js_property +* not_to_have_text +* not_to_have_value +* not_to_have_values +* to_be_attached +* to_be_checked +* to_be_disabled +* to_be_editable +* to_be_empty +* to_be_enabled +* to_be_focused +* to_be_hidden +* to_be_in_viewport +* to_be_visible +* to_contain_text +* to_have_attribute +* to_have_class +* to_have_count +* to_have_css +* to_have_id +* to_have_js_property +* to_have_text +* to_have_value +* to_have_values + ## Android * ~~connect~~ diff --git a/lib/playwright/expect.rb b/lib/playwright/expect.rb new file mode 100644 index 00000000..918debca --- /dev/null +++ b/lib/playwright/expect.rb @@ -0,0 +1,32 @@ +module Playwright + # ref: https://github.com/microsoft/playwright-python/blob/59369fe126f49c10597d5c9099840bdc8ccdcf15/playwright/sync_api/__init__.py#L90 + class Expect + def initialize + @timeout_settings = TimeoutSettings.new + end + + def call(actual, message = nil) + case actual + in Locator + LocatorAssertions.new( + LocatorAssertionsImpl.new( + actual, + @timeout_settings.timeout, + false, + message, + ) + ) + else + raise NotImplementedError.new('NOT IMPLEMENTED') + end + end + + def self.call(actual, message = nil) + self.new.call(actual, message) + end + + class << self + alias_method :[], :call + end + end +end \ No newline at end of file diff --git a/lib/playwright/locator_assertions_impl.rb b/lib/playwright/locator_assertions_impl.rb new file mode 100644 index 00000000..f3f8e39e --- /dev/null +++ b/lib/playwright/locator_assertions_impl.rb @@ -0,0 +1,404 @@ +module Playwright + # ref: https://github.com/microsoft/playwright-python/blob/main/playwright/_impl/_assertions.py + define_api_implementation :LocatorAssertionsImpl do + def self.define_negation(method_name) + define_method(:"not_#{method_name}") do |*args, **kwargs| + self.not.send(method_name, *args, **kwargs) + end + end + + def initialize(locator, timeout, is_not, message) + @locator = locator + @timeout = timeout + @is_not = is_not + @custom_message = message + end + + private def expect_impl(expression, expect_options, expected, message) + expect_options[:timeout] ||= 5000 + expect_options[:isNot] = @is_not + message.gsub!("expected to", "not expected to") if @is_not + + result = @locator.expect(expression, expect_options) + + if result["matches"] == @is_not + actual = result["received"] + + log = + if result.key?("log") + log_contents = result["log"].join("\n").strip + + "\nCall log:\n #{log_contents}" + else + "" + end + + out_message = + if @custom_message && expected + "\nExpected value: '#{expected}'" + elsif @custom_message + @custom_message + elsif message != "" && expected + "\n#{message} '#{expected}'" + else + "\n#{message}" + end + + raise RuntimeError.new("#{out_message}\nActual value #{actual} #{log}") + else + true + end + end + + private def not() + self.class.new( + @locator, + @timeout, + !@is_not, + @message + ) + end + + private def expected_regex(pattern, match_substring, normalize_white_space, ignore_case) + regex = JavaScript::Regex.new(pattern) + expected = { + regexSource: regex.source, + regexFlags: regex.flags, + matchSubstring: match_substring, + normalizeWhiteSpace: normalize_white_space, + ignoreCase: ignore_case + } + expected.delete(:ignoreCase) unless ignore_case + + expected + end + + private def to_expected_text_values(items, match_substring = false, normalize_white_space = false, ignore_case = false) + return [] unless items.respond_to?(:each) + + items.each.with_object([]) do |item, out| + out << + if item.is_a?(String) && ignore_case + { + string: item, + matchSubstring: match_substring, + normalizeWhiteSpace: normalize_white_space, + ignoreCase: ignore_case, + } + elsif item.is_a?(String) + { + string: item, + matchSubstring: match_substring, + normalizeWhiteSpace: normalize_white_space, + } + elsif item.is_a?(Pattern) + expected_regex(item, match_substring, normalize_white_space, ignore_case) + end + end + end + + def to_contain_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) + useInnerText = false if useInnerText.nil? + + if expected.respond_to?(:each) + expected_text = to_expected_text_values( + expected, + true, + true, + ignoreCase, + ) + + expect_impl( + "to.contain.text.array", + { + expectedText: expected_text, + useInnerText:, + timeout:, + }, + expected, + "Locator expected to contain text" + ) + else + expected_text = to_expected_text_values( + [expected], + true, + true, + ignoreCase + ) + + expect_impl( + "to.have.text", + { + expectedText: expected_text, + useInnerText:, + timeout:, + }, + expected, + "Locator expected to contain text" + ) + end + end + define_negation :to_contain_text + + def to_have_attribute(name, value, timeout: nil) + expected_text = to_expected_text_values([value]) + expect_impl( + "to.have.attribute.value", + { + expressionArg: name, + expectedText: expected_text, + timeout:, + }, + value, + "Locator expected to have attribute" + ) + end + define_negation :to_have_attribute + + def to_have_class(expected, timeout: nil) + if expected.respond_to?(:each) + expected_text = to_expected_text_values(expected) + expect_impl( + "to.have.class.array", + { + expectedText: expected_text, + timeout:, + }, + expected, + "Locator expected to have class" + ) + else + expected_text = to_expected_text_values([expected]) + expect_impl( + "to.have.class", + { + expectedText: expected_text, + timeout:, + }, + "Locator expected to have class" + ) + end + end + define_negation :to_have_class + + def to_have_count(count, timeout: nil) + expect_impl( + "to.have.count", + { + expectedNumber: count, + timeout:, + }, + count, + "Locator expected to have count" + ) + end + define_negation :to_have_count + + def to_have_css(name, value, timeout) + expected_text = to_expected_text_values([value]) + expect_impl( + "to.have.css", + { + expressionArg: name, + expectedText: expected_text, + timeout:, + }, + value, + "Locator expected to have CSS" + ) + end + define_negation :to_have_css + + def to_have_id(id, timeout: nil) + expected_text = to_expected_text_values([id]) + expect_impl( + "to.have.id", + { + expectedText: expected_text, + timeout: + }, + id, + "Locator expected to have ID" + ) + end + define_negation :to_have_id + + def to_have_js_property(name, value, timeout: nil) + expect_impl( + "to.have.property", + { + expressionArg: name, + expectedValue: value, + timeout:, + }, + value, + "Locator expected to have JS Property" + ) + end + define_negation :to_have_js_property + + def to_have_value(value, timeout: nil) + expected_text = to_expected_text_values([value]) + + expect_impl( + "to.have.value", + { + expectedText: expected_text, + timeout:, + }, + value, + "Locator expected to have Value" + ) + end + define_negation :to_have_value + + def to_have_values(values, timeout: nil) + expected_text = to_expected_text_values(values) + + expect_impl( + "to.have.values", + { + expectedText: expected_text, + timeout:, + }, + values, + "Locator expected to have Values" + ) + end + define_negation :to_have_values + + def to_have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) + if expected.respond_to?(:each) + expected_text = to_expected_text_values( + expected, + true, + ignoreCase, + ) + expect_impl( + "to.have.text.array", + { + expectedText: expected_text, + userInnerText: useInnerText, + timeout:, + }, + expected, + "Locator expected to have text" + ) + else + expected_text = to_expected_text_values([expected], true, ignoreCase) + expect_impl( + "to.have.text", + { + expectedText: expected_text, + useInnerText: useInnerText, + timeout:, + }, + expected, + "Locator expected to have text" + ) + end + end + define_negation :to_have_text + + def to_be_attached(attached: nil, timeout: nil) + expect_impl( + (attached || attached.nil?) ? "to.be.attached" : "to.be.detached", + { timeout: }, + nil, + "Locator expected to be attached" + ) + end + define_negation :to_be_attached + + def to_be_checked(checked: nil, timeout: nil) + expect_impl( + (checked || checked.nil?) ? "to.be.checked" : "to.be.unchecked", + { timeout: }, + nil, + "Locator expected to be checked" + ) + end + define_negation :to_be_checked + + def to_be_disabled(timeout: nil) + expect_impl( + "to.be.disabled", + { timeout: }, + nil, + "Locator expected to be disabled" + ) + end + define_negation :to_be_disabled + + def to_be_editable(editable: nil, timeout: nil) + expect_impl( + (editable || editable.nil?) ? "to.be.editable" : "to.be.readonly", + { timeout: }, + nil, + "Locator expected to be editable" + ) + end + define_negation :to_be_editable + + def to_be_empty(timeout: nil) + expect_impl( + "to.be.empty", + { timeout: }, + nil, + "Locator expected to be empty" + ) + end + define_negation :to_be_empty + + def to_be_enabled(enabled: nil, timeout: nil) + expect_impl( + (enabled || enabled.nil?) ? "to.be.enabled" : "to.be.disabled", + { timeout: }, + nil, + "Locator expected to be enabled" + ) + end + define_negation :to_be_enabled + + def to_be_hidden(timeout: nil) + expect_impl( + "to.be.hidden", + { timeout: }, + nil, + "Locator expected to be hidden" + ) + end + define_negation :to_be_hidden + + def to_be_visible(timeout: nil, visible: nil) + expect_impl( + (visible || visible.nil?) ? "to.be.visible" : "to.be.hidden", + { timeout: }, + nil, + "Locator expected to be visible" + ) + end + define_negation :to_be_visible + + def to_be_focused(timeout: nil) + expect_impl( + "to.be.focused", + { timeout: }, + nil, + "Locator expected to be focused" + ) + end + define_negation :to_be_focused + + def to_be_in_viewport(ratio: nil, timeout: nil) + expect_impl( + "to.be.in.viewport", + { timeout:, expectedNumber: ratio }, + nil, + "Locator expected to be in viewport" + ) + end + define_negation :to_be_in_viewport + + end +end \ No newline at end of file From 269eabb6cc427990a224c44e0e52822db398a7a1 Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Sat, 28 Oct 2023 20:41:24 +0100 Subject: [PATCH 3/8] rspec integration --- lib/playwright/errors.rb | 2 + lib/playwright/expect.rb | 32 ---------- lib/playwright/locator_assertions_impl.rb | 3 +- lib/playwright/test.rb | 68 +++++++++++++++++++++ spec/integration/locator_assertions_spec.rb | 5 ++ 5 files changed, 77 insertions(+), 33 deletions(-) delete mode 100644 lib/playwright/expect.rb create mode 100644 lib/playwright/test.rb create mode 100644 spec/integration/locator_assertions_spec.rb diff --git a/lib/playwright/errors.rb b/lib/playwright/errors.rb index ca78839c..b3819553 100644 --- a/lib/playwright/errors.rb +++ b/lib/playwright/errors.rb @@ -49,4 +49,6 @@ def initialize(error, page) attr_reader :error, :page end + + class AssertionError < StandardError; end end diff --git a/lib/playwright/expect.rb b/lib/playwright/expect.rb deleted file mode 100644 index 918debca..00000000 --- a/lib/playwright/expect.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Playwright - # ref: https://github.com/microsoft/playwright-python/blob/59369fe126f49c10597d5c9099840bdc8ccdcf15/playwright/sync_api/__init__.py#L90 - class Expect - def initialize - @timeout_settings = TimeoutSettings.new - end - - def call(actual, message = nil) - case actual - in Locator - LocatorAssertions.new( - LocatorAssertionsImpl.new( - actual, - @timeout_settings.timeout, - false, - message, - ) - ) - else - raise NotImplementedError.new('NOT IMPLEMENTED') - end - end - - def self.call(actual, message = nil) - self.new.call(actual, message) - end - - class << self - alias_method :[], :call - end - end -end \ No newline at end of file diff --git a/lib/playwright/locator_assertions_impl.rb b/lib/playwright/locator_assertions_impl.rb index f3f8e39e..be17c654 100644 --- a/lib/playwright/locator_assertions_impl.rb +++ b/lib/playwright/locator_assertions_impl.rb @@ -44,7 +44,8 @@ def initialize(locator, timeout, is_not, message) "\n#{message}" end - raise RuntimeError.new("#{out_message}\nActual value #{actual} #{log}") + out = "#{out_message}\nActual value #{actual} #{log}" + raise AssertionError.new(out) else true end diff --git a/lib/playwright/test.rb b/lib/playwright/test.rb new file mode 100644 index 00000000..72cd4cbe --- /dev/null +++ b/lib/playwright/test.rb @@ -0,0 +1,68 @@ +module Playwright + # this module is responsible for running playwright assertions and integrating + # with test frameworks. + module Test + # ref: https://github.com/microsoft/playwright-python/blob/main/playwright/sync_api/__init__.py#L90 + class Expect + def initialize + @timeout_settings = TimeoutSettings.new + end + + def call(actual, message = nil) + if actual.is_a?(Locator) + LocatorAssertions.new( + LocatorAssertionsImpl.new( + actual, + @timeout_settings.timeout, + false, + message, + ) + ) + else + raise NotImplementedError.new("Only locator assertions are currently implemented") + end + end + end + + module RSpec + class PlaywrightMatcher + def initialize(expectation_method, *args, **kwargs) + @method = expectation_method + @args = args + @kwargs = kwargs + end + + def matches?(actual) + Expect.new.call(actual).send(@method, *@args, **@kwargs) + true + rescue AssertionError => e + @failure_message = e.full_message + false + end + + def failure_message + @failure_message + end + + # we have to invert the message again here because RSpec wants to control + # its own negation + def failure_message_when_negated + @failure_message.gsub("expected to", "not expected to") + end + end + end + + ALL_ASSERTIONS = LocatorAssertions.instance_methods(false) + + ALL_ASSERTIONS + .map(&:to_s) + .each do |method_name| + # to_be_visible => be_visible + # not_to_be_visible => not_be_visible + root_method_name = method_name.gsub("to_", "") + RSpec.define_method(root_method_name) do |*args, **kwargs| + RSpec::PlaywrightMatcher.new(method_name, *args, **kwargs) + end + end + end +end \ No newline at end of file diff --git a/spec/integration/locator_assertions_spec.rb b/spec/integration/locator_assertions_spec.rb new file mode 100644 index 00000000..a1872532 --- /dev/null +++ b/spec/integration/locator_assertions_spec.rb @@ -0,0 +1,5 @@ +require "spec_helper" + +Expect = Playwright::Expect +RSpec.describe Playwright::LocatorAssertions do +end \ No newline at end of file From ac7ade32812dc4b9433263a84e55a8cde9488da9 Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:52:22 +0100 Subject: [PATCH 4/8] start implementing tests --- lib/playwright/javascript/value_serializer.rb | 2 +- lib/playwright/locator_assertions_impl.rb | 7 +- lib/playwright/locator_impl.rb | 6 +- spec/integration/locator_assertions_spec.rb | 199 +++++++++++++++++- 4 files changed, 205 insertions(+), 9 deletions(-) diff --git a/lib/playwright/javascript/value_serializer.rb b/lib/playwright/javascript/value_serializer.rb index ef4988fd..5eb4e120 100644 --- a/lib/playwright/javascript/value_serializer.rb +++ b/lib/playwright/javascript/value_serializer.rb @@ -24,7 +24,7 @@ def serialize @handles << value.channel { h: index } when nil - { v: 'undefined' } + { v: 'null' } when Float::NAN { v: 'NaN'} when Float::INFINITY diff --git a/lib/playwright/locator_assertions_impl.rb b/lib/playwright/locator_assertions_impl.rb index be17c654..c49868b7 100644 --- a/lib/playwright/locator_assertions_impl.rb +++ b/lib/playwright/locator_assertions_impl.rb @@ -64,7 +64,7 @@ def initialize(locator, timeout, is_not, message) regex = JavaScript::Regex.new(pattern) expected = { regexSource: regex.source, - regexFlags: regex.flags, + regexFlags: regex.flag, matchSubstring: match_substring, normalizeWhiteSpace: normalize_white_space, ignoreCase: ignore_case @@ -92,7 +92,7 @@ def initialize(locator, timeout, is_not, message) matchSubstring: match_substring, normalizeWhiteSpace: normalize_white_space, } - elsif item.is_a?(Pattern) + elsif item.is_a?(Regexp) expected_regex(item, match_substring, normalize_white_space, ignore_case) end end @@ -176,6 +176,7 @@ def to_have_class(expected, timeout: nil) expectedText: expected_text, timeout:, }, + expected, "Locator expected to have class" ) end @@ -195,7 +196,7 @@ def to_have_count(count, timeout: nil) end define_negation :to_have_count - def to_have_css(name, value, timeout) + def to_have_css(name, value, timeout: nil) expected_text = to_expected_text_values([value]) expect_impl( "to.have.css", diff --git a/lib/playwright/locator_impl.rb b/lib/playwright/locator_impl.rb index b4eed577..8b115c01 100644 --- a/lib/playwright/locator_impl.rb +++ b/lib/playwright/locator_impl.rb @@ -490,9 +490,9 @@ def highlight end def expect(expression, options) - if options.key? :expected_value - options[:expected_value] = JavaScript::ValueSerializer - .new(options[:expected_value]) + if options.key? :expectedValue + options[:expectedValue] = JavaScript::ValueSerializer + .new(options[:expectedValue]) .serialize end diff --git a/spec/integration/locator_assertions_spec.rb b/spec/integration/locator_assertions_spec.rb index a1872532..f8ad2bc5 100644 --- a/spec/integration/locator_assertions_spec.rb +++ b/spec/integration/locator_assertions_spec.rb @@ -1,5 +1,200 @@ require "spec_helper" +require "playwright/test" -Expect = Playwright::Expect -RSpec.describe Playwright::LocatorAssertions do +# ref: https://github.com/microsoft/playwright-python/blob/main/tests/sync/test_assertions.py +RSpec.describe Playwright::LocatorAssertions, sinatra: true do + include Playwright::Test::RSpec + + it "should work with #to_contain_text" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
") + expect(page.locator("div#foobar")).to contain_text("kek") + expect(page.locator("div#foobar")).to not_contain_text("bar", timeout: 100) + + expect { + expect(page.locator("div#foobar")).to contain_text("bar", timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + + page.set_content("
Text \n1
Text2
Text3
") + expect(page.locator("div")).to contain_text(["ext 1", /ext3/]) + end + end + + it "should work with #to_have_attribute" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
") + expect(page.locator("div#foobar")).to have_attribute("id", "foobar") + expect(page.locator("div#foobar")).to have_attribute("id", /foobar/) + expect(page.locator("div#foobar")).to not_have_attribute("id", "kek", timeout: 100) + + expect { + expect(page.locator("div#foobar")).to have_attribute("id", "koko", timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_class" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
") + expect(page.locator("div.foobar")).to have_class("foobar") + expect(page.locator("div.foobar")).to have_class(["foobar"]) + expect(page.locator("div.foobar")).to have_class(/foobar/) + expect(page.locator("div.foobar")).to not_have_class("kekstar", timeout: 100) + + expect { + expect(page.locator("div.foobar")).to have_class("oh-no", timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_count" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
kek
") + expect(page.locator("div.foobar")).to have_count(2) + expect(page.locator("div.foobar")).to not_have_count(42, timeout: 100) + + expect { + expect(page.locator("div.foobar")).to have_count(42, timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_css" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
") + expect(page.locator("div.foobar")).to have_css("color", "rgb(234, 74, 90)") + expect(page.locator("div.foobar")).to not_have_css( + "color", "rgb(42, 42, 42)", timeout: 100) + + expect { + expect(page.locator("div.foobar")).to have_css( + "color", "rgb(42, 42, 42)", timeout: 100 + ) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_id" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
") + expect(page.locator("div.foobar")).to have_id("kek") + expect(page.locator("div.foobar")).to not_have_id("top", timeout: 100) + + expect { + expect(page.locator("div.foobar")).to have_id("top", timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_js_property" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
") + page.eval_on_selector( + "div", "e => e.foo = { a: 1, b: 'string', c: new Date(1627503992000) }" + ) + expect(page.locator("div")).to have_js_property( + "foo", + { "a" => 1, "b" => "string", "c" => Time.at(1627503992000 / 1000) } + ) + end + end + + it "should work with #to_have_js_property pass string" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + expect(locator).to have_js_property("foo", "string") + end + end + + it "should work with #to_have_js_property fail string" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + expect { + expect(locator).to have_js_property("foo", "error", timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_js_property pass number" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + expect(locator).to have_js_property("foo", 2021) + end + end + + it "should work with #to_have_js_property fail number" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + + expect { + expect(locator).to have_js_property("foo", 1, timeout: 500) + } + end + end + + it "should work with #to_have_js_property pass boolean" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = true") + locator = page.locator("div") + expect(locator).to have_js_property("foo", true) + end + end + + it "should work with #to_have_js_property fail boolean" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + + expect { + expect(locator).to have_js_property("foo", true, timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_js_property pass boolean 2" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + expect(locator).to have_js_property("foo", false) + end + end + + it "should work with #to_have_js_property fail boolean 2" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = true") + locator = page.locator("div") + + expect { + expect(locator).to have_js_property("foo", false, timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with #to_have_js_property pass null" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = null") + locator = page.locator("div") + expect(locator).to have_js_property("foo", nil) + end + end end \ No newline at end of file From 7feefac516e5d81344a2f4c5bf319ffff2cb21f9 Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:53:02 +0100 Subject: [PATCH 5/8] gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 4b4efe2a..95e1511f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ # RubyMine /.idea/ /.rakeTasks + +package.json +package-lock.json +yarn.lock +node_modules \ No newline at end of file From db0e067d8784a61322433ab5b7fdd4941f44fd8a Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Sun, 29 Oct 2023 14:05:38 +0000 Subject: [PATCH 6/8] more integration tests --- lib/playwright/locator_assertions_impl.rb | 11 +- spec/integration/locator_assertions_spec.rb | 596 ++++++++++++++++++-- 2 files changed, 546 insertions(+), 61 deletions(-) diff --git a/lib/playwright/locator_assertions_impl.rb b/lib/playwright/locator_assertions_impl.rb index c49868b7..bfbb5f7f 100644 --- a/lib/playwright/locator_assertions_impl.rb +++ b/lib/playwright/locator_assertions_impl.rb @@ -18,6 +18,7 @@ def initialize(locator, timeout, is_not, message) expect_options[:timeout] ||= 5000 expect_options[:isNot] = @is_not message.gsub!("expected to", "not expected to") if @is_not + expect_options.delete(:useInnerText) if expect_options.key?(:useInnerText) && expect_options[:useInnerText].nil? result = @locator.expect(expression, expect_options) @@ -69,7 +70,7 @@ def initialize(locator, timeout, is_not, message) normalizeWhiteSpace: normalize_white_space, ignoreCase: ignore_case } - expected.delete(:ignoreCase) unless ignore_case + expected.delete(:ignoreCase) if ignore_case.nil? expected end @@ -274,6 +275,7 @@ def to_have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) expected_text = to_expected_text_values( expected, true, + true, ignoreCase, ) expect_impl( @@ -287,7 +289,12 @@ def to_have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil) "Locator expected to have text" ) else - expected_text = to_expected_text_values([expected], true, ignoreCase) + expected_text = to_expected_text_values( + [expected], + true, + true, + ignoreCase, + ) expect_impl( "to.have.text", { diff --git a/spec/integration/locator_assertions_spec.rb b/spec/integration/locator_assertions_spec.rb index f8ad2bc5..8e1478e8 100644 --- a/spec/integration/locator_assertions_spec.rb +++ b/spec/integration/locator_assertions_spec.rb @@ -106,95 +106,573 @@ end end - it "should work with #to_have_js_property pass string" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = 'string'") - locator = page.locator("div") - expect(locator).to have_js_property("foo", "string") + describe "#to_have_js_property" do + it "should work with pass string" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + expect(locator).to have_js_property("foo", "string") + end + end + + it "should work with fail string" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 'string'") + locator = page.locator("div") + expect { + expect(locator).to have_js_property("foo", "error", timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with pass number" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + expect(locator).to have_js_property("foo", 2021) + end + end + + it "should work with fail number" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = 2021") + locator = page.locator("div") + + expect { + expect(locator).to have_js_property("foo", 1, timeout: 500) + } + end + end + + it "should work with pass boolean" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = true") + locator = page.locator("div") + expect(locator).to have_js_property("foo", true) + end + end + + it "should work with fail boolean" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + + expect { + expect(locator).to have_js_property("foo", true, timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with pass boolean 2" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = false") + locator = page.locator("div") + expect(locator).to have_js_property("foo", false) + end + end + + it "should work with fail boolean 2" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = true") + locator = page.locator("div") + + expect { + expect(locator).to have_js_property("foo", false, timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with pass null" do + with_page do |page| + page.set_content("
") + page.eval_on_selector("div", "e => e.foo = null") + locator = page.locator("div") + expect(locator).to have_js_property("foo", nil) + end end end - it "should work with #to_have_js_property fail string" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = 'string'") - locator = page.locator("div") - expect { - expect(locator).to have_js_property("foo", "error", timeout: 500) - }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + describe "#to_have_text" do + it "should work" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
kek
") + expect(page.locator("div#foobar")).to have_text("kek") + expect(page.locator("div#foobar")).to not_contain_text("top", timeout: 100) + + page.set_content("
Text \n1
Text 2a
") + expect(page.locator("div")).to have_text( + ["Text 1", /Text \d+a/] + ) + end + end + + it "should ignore case" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
apple BANANA
orange
") + expect(page.locator("div#target")).to have_text("apple BANANA") + expect(page.locator("div#target")).to have_text("apple banana", ignoreCase: true) + + # defaults false + expect { + expect(page.locator("div#target")).to have_text("apple banana", timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to have_text("apple banana", timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + + # array variant + expect(page.locator("div")).to have_text(["apple BANANA", "orange"]) + expect(page.locator("div")).to have_text(["apple banana", "ORANGE"], ignoreCase: true) + + # defaults false + expect { + expect(page.locator("div")).to have_text(["apple banana", "ORANGE"], timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to have_text("apple banana", timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + + # not variant + expect(page.locator("div#target")).to not_have_text("apple banana") + expect { + expect(page.locator("div#target")).to not_have_text("apple banana", ignoreCase: true, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to not_have_text("apple banana", ignoreCase: true, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + end + end + + it "should ignore case regex" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
apple BANANA
orange
") + expect(page.locator("div#target")).to have_text(/apple BANANA/) + expect(page.locator("div#target")).to have_text(/apple banana/, ignoreCase: true) + + expect { + expect(page.locator("div#target")).to have_text(/apple banana/, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to have_text(/apple banana/, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + + expect { + expect(page.locator("div#target")).to have_text(/apple banana/i, ignoreCase: false, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to have_text(/apple banana/i, ignoreCase: false, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + + # array variant + expect(page.locator("div")).to have_text([/apple BANANA/, /orange/]) + expect(page.locator("div")).to have_text([/apple banana/, /ORANGE/], ignoreCase: true) + + # defaults regex flag + expect { + expect(page.locator("div")).to have_text([/apple banana/, /ORANGE/], timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div")).to have_text([/apple banana/, /ORANGE/], timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + + # overrides regex flag + expect { + expect(page.locator("div")).to have_text([/apple banana/i, /ORANGE/i], ignoreCase: false, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div")).to have_text([/apple banana/i, /ORANGE/i], ignoreCase: false, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to have text") + end + + # not variant + expect(page.locator("div#target")).to not_have_text(/apple banana/) + expect { + expect(page.locator("div#target")).to not_have_text(/apple banana/, ignoreCase: true, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to not_have_text(/apple banana/, ignoreCase: true, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("not expected to have text") + end + end end end - it "should work with #to_have_js_property pass number" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = 2021") - locator = page.locator("div") - expect(locator).to have_js_property("foo", 2021) + describe "#to_contain_text" do + it "should ignore case" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
apple BANANA
orange
") + expect(page.locator("div#target")).to contain_text("apple BANANA") + expect(page.locator("div#target")).to contain_text("apple banana", ignoreCase: true) + + # defaults false + expect { + expect(page.locator("div#target")).to contain_text("apple banana", timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to contain_text("apple banana", timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + + # array variant + expect(page.locator("div")).to contain_text(["apple BANANA", "orange"]) + expect(page.locator("div")).to contain_text(["apple banana", "ORANGE"], ignoreCase: true) + + # defaults false + expect { + expect(page.locator("div")).to contain_text(["apple banana", "ORANGE"], timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to contain_text("apple banana", timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + + # not variant + expect(page.locator("div#target")).to not_contain_text("apple banana") + expect { + expect(page.locator("div#target")).to not_contain_text("apple banana", ignoreCase: true, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to not_contain_text("apple banana", ignoreCase: true, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + end + end + + it "should ignore case regex" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
apple BANANA
orange
") + expect(page.locator("div#target")).to contain_text(/apple BANANA/) + expect(page.locator("div#target")).to contain_text(/apple banana/, ignoreCase: true) + + expect { + expect(page.locator("div#target")).to contain_text(/apple banana/, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to contain_text(/apple banana/, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + + expect { + expect(page.locator("div#target")).to contain_text(/apple banana/i, ignoreCase: false, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to contain_text(/apple banana/i, ignoreCase: false, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + + # array variant + expect(page.locator("div")).to contain_text([/apple BANANA/, /orange/]) + expect(page.locator("div")).to contain_text([/apple banana/, /ORANGE/], ignoreCase: true) + + # defaults regex flag + expect { + expect(page.locator("div")).to contain_text([/apple banana/, /ORANGE/], timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div")).to contain_text([/apple banana/, /ORANGE/], timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + + # overrides regex flag + expect { + expect(page.locator("div")).to contain_text([/apple banana/i, /ORANGE/i], ignoreCase: false, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div")).to contain_text([/apple banana/i, /ORANGE/i], ignoreCase: false, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("to contain text") + end + + # not variant + expect(page.locator("div#target")).to not_contain_text(/apple banana/) + expect { + expect(page.locator("div#target")).to not_contain_text(/apple banana/, ignoreCase: true, timeout: 300) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(page.locator("div#target")).to not_contain_text(/apple banana/, ignoreCase: true, timeout: 300) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("not expected to contain text") + end + end + end + + it "should work with #to_have_value" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("") + my_input = page.locator("#foo") + expect(my_input).to have_value("") + expect(my_input).to not_have_value("bar", timeout: 100) + my_input.fill("kektus") + expect(my_input).to have_value("kektus") + end end end - it "should work with #to_have_js_property fail number" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = 2021") - locator = page.locator("div") + describe "#to_have_values" do + it "should work with text" do + with_page do |page| + page.set_content(<<~HTML) + + HTML - expect { - expect(locator).to have_js_property("foo", 1, timeout: 500) - } + locator = page.locator("select") + locator.select_option(value: ["R", "G"]) + expect(locator).to have_values(["R", "G"]) + end end - end - it "should work with #to_have_js_property pass boolean" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = true") - locator = page.locator("div") - expect(locator).to have_js_property("foo", true) + it "should follow labels" do + with_page do |page| + page.set_content(<<~HTML) + + + HTML + + locator = page.locator("text=Pick a color") + locator.select_option(value: ["R", "G"]) + expect(locator).to have_values(["R", "G"]) + end + end + + it "must exactly match text" do + with_page do |page| + page.set_content(<<~HTML) + + HTML + + locator = page.locator("select") + locator.select_option(value: ["RR", "GG"]) + expect { + expect(locator).to have_values(["R", "G"], timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(locator).to have_values(["R", "G"], timeout: 500) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("Locator expected to have Values '[\"R\", \"G\"]'") + actual_value = '{"a"=>[{"s"=>"RR"}, {"s"=>"GG"}], "id"=>1}' + expect(e.full_message).to include("Actual value #{actual_value}") # TODO: print actual value in prettier format? + end + end + end + + it "should work with regex" do + with_page do |page| + page.set_content(<<~HTML) + + HTML + + locator = page.locator("select") + locator.select_option(value: ["R", "G"]) + expect(locator).to have_values([/R/, /G/]) + end + end + + it "should work when items not selected" do + with_page do |page| + page.set_content(<<~HTML) + + HTML + + locator = page.locator("select") + locator.select_option(value: ["B"]) + expect { + expect(locator).to have_values(["R", "G"], timeout: 500) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + begin + expect(locator).to have_values(["R", "G"], timeout: 500) + rescue RSpec::Expectations::ExpectationNotMetError => e + expect(e.full_message).to include("Locator expected to have Values '[\"R\", \"G\"]'") + actual_value = '{"a"=>[{"s"=>"B"}], "id"=>1}' + expect(e.full_message).to include("Actual value #{actual_value}") # TODO: print actual value in prettier format? + end + end + end + + it "should fail when multiple not specified" do + with_page do |page| + page.set_content(<<~HTML) + + HTML + + locator = page.locator("select") + locator.select_option(value: ["B"]) + expect { + expect(locator).to have_values(["R", "G"], timeout: 500) + }.to raise_error(Playwright::Error) + begin + expect(locator).to have_values(["R", "G"], timeout: 500) + rescue Playwright::Error => e + expect(e.full_message).to include("Error: Not a select element with a multiple attribute") + end + end + end + + it "should fail when not a select element" do + with_page do |page| + page.set_content("") + locator = page.locator("input") + expect { + expect(locator).to have_values(["R", "G"], timeout: 500) + }.to raise_error(Playwright::Error) + begin + expect(locator).to have_values(["R", "G"], timeout: 500) + rescue Playwright::Error => e + expect(e.full_message).to include("Error: Not a select element with a multiple attribute") + end + end end end - it "should work with #to_have_js_property fail boolean" do + it "works with #to_be_checked" do with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = false") - locator = page.locator("div") + page.goto(server_empty_page) + page.set_content("") + my_checkbox = page.locator("input") + expect(my_checkbox).to not_be_checked expect { - expect(locator).to have_js_property("foo", true, timeout: 500) + expect(my_checkbox).to be_checked(timeout: 100) }.to raise_error(RSpec::Expectations::ExpectationNotMetError) - end - end - it "should work with #to_have_js_property pass boolean 2" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = false") - locator = page.locator("div") - expect(locator).to have_js_property("foo", false) + expect(my_checkbox).to be_checked(timeout: 100, checked: false) + + expect { + expect(my_checkbox).to be_checked(timeout: 100, checked: true) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + + my_checkbox.check() + expect(my_checkbox).to be_checked(timeout: 100, checked: true) + + expect { + expect(my_checkbox).to be_checked(timeout: 100, checked: false) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + + expect(my_checkbox).to be_checked end end - it "should work with #to_have_js_property fail boolean 2" do + it "should work with #to_be_enabled / #to_be_disabled" do with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = true") - locator = page.locator("div") + page.goto(server_empty_page) + page.set_content("") + my_checkbox = page.locator("input") + expect(my_checkbox).to not_be_disabled + expect(my_checkbox).to be_enabled + + expect { + expect(my_checkbox).to be_disabled(timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + my_checkbox.evaluate("e => e.disabled = true") + expect(my_checkbox).to be_disabled expect { - expect(locator).to have_js_property("foo", false, timeout: 500) + expect(my_checkbox).to be_enabled(timeout: 100) }.to raise_error(RSpec::Expectations::ExpectationNotMetError) end end - it "should work with #to_have_js_property pass null" do - with_page do |page| - page.set_content("
") - page.eval_on_selector("div", "e => e.foo = null") - locator = page.locator("div") - expect(locator).to have_js_property("foo", nil) + describe "#to_be_enabled" do + it "should work with true" do + with_page do |page| + page.set_content("") + expect(page.locator("button")).to be_enabled(enabled: true) + end + end + + it "should work with false" do + with_page do |page| + page.set_content("") + expect(page.locator("button")).to be_enabled(enabled: false) + end + end + + it "should work with not and false" do + with_page do |page| + page.set_content("") + expect(page.locator("button")).to not_be_enabled(enabled: false) + end + end + + it "should work eventually" do + with_page do |page| + page.set_content("") + page.eval_on_selector("button", <<~JS) + button => setTimeout(() => { + button.removeAttribute('disabled') + }, 700) + JS + expect(page.locator("button")).to be_enabled + end + end + + it "should work eventually with not" do + with_page do |page| + page.set_content("") + page.eval_on_selector("button", <<~JS) + button => setTimeout(() => { + button.setAttribute('disabled', '') + }, 700) + JS + expect(page.locator("button")).to not_be_enabled + end end end + end \ No newline at end of file From 8c5c21cbee3639217340a4b0b992f1581bc80d2c Mon Sep 17 00:00:00 2001 From: Alex Brook <90186562+abr-storm@users.noreply.github.com> Date: Sun, 29 Oct 2023 14:46:41 +0000 Subject: [PATCH 7/8] rename matchers module, finish integration tests --- lib/playwright/test.rb | 6 +- spec/integration/locator_assertions_spec.rb | 155 +++++++++++++++++++- 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/lib/playwright/test.rb b/lib/playwright/test.rb index 72cd4cbe..70ce7efe 100644 --- a/lib/playwright/test.rb +++ b/lib/playwright/test.rb @@ -24,7 +24,7 @@ def call(actual, message = nil) end end - module RSpec + module Matchers class PlaywrightMatcher def initialize(expectation_method, *args, **kwargs) @method = expectation_method @@ -60,8 +60,8 @@ def failure_message_when_negated # to_be_visible => be_visible # not_to_be_visible => not_be_visible root_method_name = method_name.gsub("to_", "") - RSpec.define_method(root_method_name) do |*args, **kwargs| - RSpec::PlaywrightMatcher.new(method_name, *args, **kwargs) + Matchers.define_method(root_method_name) do |*args, **kwargs| + Matchers::PlaywrightMatcher.new(method_name, *args, **kwargs) end end end diff --git a/spec/integration/locator_assertions_spec.rb b/spec/integration/locator_assertions_spec.rb index 8e1478e8..46415049 100644 --- a/spec/integration/locator_assertions_spec.rb +++ b/spec/integration/locator_assertions_spec.rb @@ -3,7 +3,7 @@ # ref: https://github.com/microsoft/playwright-python/blob/main/tests/sync/test_assertions.py RSpec.describe Playwright::LocatorAssertions, sinatra: true do - include Playwright::Test::RSpec + include Playwright::Test::Matchers it "should work with #to_contain_text" do with_page do |page| @@ -320,6 +320,31 @@ end end end + + it "should be able to serialize regex correctly" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
iGnOrEcAsE
") + expect(page.locator("div")).to have_text(/ignorecase/i) + + page.set_content(<<~HTML) +
start + + some + lines + between + end
+ HTML + expect(page.locator("div")).to have_text(/start.*end/m) + + page.set_content(<<~HTML) +
line1 + line2 + line3
+ HTML + expect(page.locator("div")).to have_text(/^line2$/m) + end + end end describe "#to_contain_text" do @@ -675,4 +700,132 @@ end end + describe "#to_be_editable" do + it "should work" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("") + expect(page.locator("button")).to not_be_editable + expect(page.locator("input")).to be_editable + expect { + expect(page.locator("button")).to be_editable(timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + it "should work with true" do + with_page do |page| + page.set_content("") + expect(page.locator("input")).to be_editable(editable: true) + end + end + + it "should work with false" do + with_page do |page| + page.set_content("") + expect(page.locator("input")).to be_editable(editable: false) + end + end + + it "should work with not and false" do + with_page do |page| + page.set_content("") + expect(page.locator("input")).to not_be_editable(editable: false) + end + end + end + + it "should work with #to_be_empty" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("") + expect(page.locator("input[name=input1]")).to not_be_empty + expect(page.locator("input[name=input2]")).to be_empty + expect { + expect(page.locator("input[name=input1]")).to be_empty(timeout: 100) + } + end + end + + it "should work with #to_be_focused" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("") + my_checkbox = page.locator("input") + expect { + expect(my_checkbox).to be_focused(timeout: 100) + } + my_checkbox.focus() + expect(my_checkbox).to be_focused + end + end + + it "should work with #to_be_hidden / #to_be_visible" do + with_page do |page| + page.goto(server_empty_page) + page.set_content("
Something
") + my_checkbox = page.locator("div") + expect(my_checkbox).to be_visible + expect { + expect(my_checkbox).to be_hidden(timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + + my_checkbox.evaluate("e => e.style.display = 'none'") + expect(my_checkbox).to be_hidden + + expect { + expect(my_checkbox).to be_visible(timeout: 100) + }.to raise_error(RSpec::Expectations::ExpectationNotMetError) + end + end + + describe "#to_be_visible" do + it "should work with true" do + with_page do |page| + page.set_content("