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
+
+
Item Text 1
+
Item Text 2
+
Item Text 3
+
+```
+
+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
+
+
Text 1
+
Text 2
+
Text 3
+
+```
+
+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("
")
+ 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("
+ 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("hellohello")
+ page.eval_on_selector("div", <<~JS)
+ div => setTimeout(() => {
+ div.innerHTML = 'Hello'
+ }, 700)
+ JS
+ expect(page.locator("span")).to be_visible
+ end
+ end
+
+ it "should work eventually with not" do
+ with_page do |page|
+ page.set_content("
Hello
")
+ page.eval_on_selector("span", <<~JS)
+ span => setTimeout(() => {
+ span.textContent = ''
+ }, 700)
+ JS
+
+ expect(page.locator("span")).to not_be_visible
+ end
+ end
+ end
+
+
end
\ No newline at end of file
From fe2a0a314aa6b1553cb09da8beaca1d8f0e9dd5c Mon Sep 17 00:00:00 2001
From: Alex Brook <90186562+abr-storm@users.noreply.github.com>
Date: Sun, 29 Oct 2023 15:43:52 +0000
Subject: [PATCH 8/8] matcher example in documentation
---
documentation/docs/api/locator_assertions.md | 23 ++++++++++++++------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/documentation/docs/api/locator_assertions.md b/documentation/docs/api/locator_assertions.md
index da3a61b6..d701e07b 100644
--- a/documentation/docs/api/locator_assertions.md
+++ b/documentation/docs/api/locator_assertions.md
@@ -7,13 +7,22 @@ sidebar_position: 10
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")
+```ruby sync title=example_eff0600f575bf375d7372280ca8e6dfc51d927ced49fbcb75408c894b9e0564e.py
+require "playwright/test"
+
+# Every locator assertion has a corresponding matcher. For example:
+#
+# to_be_visible => expect(my_locator).to be_visible
+# not_to_be_visible => expect(my_locator).to not_be_visible
+
+RSpec.describe "My feature", type: :feature do
+ include Playwright::Test::Matchers
+
+ it "changes the status to submitted" do
+ page.get_by_role("button").click
+ expect(page.locator(".status")).to have_text("Submitted")
+ end
+end
```