e2e 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 803c14bd6d5868f7ae89a2af6e67da28be4fa3a8fc48249a0a60672fad495c3c
4
- data.tar.gz: 5e51d72904f0ccb1d316f753e4fbd766957de8538ce804a8a8bf2a8ae26f911c
3
+ metadata.gz: d1381ff492123e32a28b99424261800a7a7b2af8fe6ad9e062ac15decf621718
4
+ data.tar.gz: e80efe7eabc1c5ffd43802138cbce63cbb824558d22439b4e2702f62c4bb8ef3
5
5
  SHA512:
6
- metadata.gz: 98cd206b7dc00293712b48a93d5ff732d01cf39630ebfbe2d0a105bb7f9d34a0efe59c8d16d93149774ed76709450d925e58eb376669f11cd3257723a82852e3
7
- data.tar.gz: febfd9530d491c8c2ec6b272040833a5fc2d5c4f3fc49cd8eb3c4920294f11ef44451216222aa4d4646b388f66b2e32c9d424d921e335fbe852c99437dd46111
6
+ metadata.gz: 1e849aacf7b8ccac13c94c3724a407ca7f0f3f563a2aa0ef6d12bee3c9383db32a1a73460d02af96c38960d7a68997ca3dcf0d167b42bafeb142b12ab2afb675
7
+ data.tar.gz: 8f235e5164cf96c3a93d3fde923ed9666482cc13765b566a8cf713900d611165ebb9a8d84aee387fc8478271013deb1276fe0dc9bd888cfa4819c8425d4f15d8
data/CHANGELOG.md CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-02-06
11
+
12
+ ### Added
13
+
14
+ - **Auto-waiting matchers:** `have_text`, `have_content`, and `have_current_path` now automatically retry until the expected condition is met or the timeout expires, eliminating flaky tests caused by page transitions and async rendering
15
+ - `have_current_path` matcher for asserting the current URL path with auto-waiting (supports exact strings and regexps)
16
+ - `E2E.wait_until` helper method for custom waiting logic in tests
17
+ - `wait_timeout` configuration option (default: 5 seconds) to control how long matchers wait before failing
18
+ - `text` method on page/session for reading visible text content of the page body
19
+
20
+ ### Changed
21
+
22
+ - `fill_in` now matches fields by label, placeholder, id, and name (Capybara-like behavior) instead of only CSS selectors, with fallback to direct CSS selector
23
+
10
24
  ## [0.3.2] - 2026-02-05
11
25
 
12
26
  ### Fixed
data/README.md CHANGED
@@ -85,7 +85,8 @@ RSpec.describe "User Login", type: :e2e do
85
85
  fill_in "Password", with: "password"
86
86
  click_button "Sign In"
87
87
 
88
- expect(page.body).to include("Welcome, User!")
88
+ expect(page).to have_current_path("/dashboard")
89
+ expect(page).to have_content("Welcome, User!")
89
90
  end
90
91
  end
91
92
  ```
@@ -159,7 +160,8 @@ click_button "Submit"
159
160
  click_link "Read more"
160
161
  click "#nav-menu" # CSS selector
161
162
 
162
- fill_in "Email", with: "test@example.com"
163
+ fill_in "Email", with: "test@example.com" # Matches by label, placeholder, id, or name
164
+ fill_in "#email", with: "test@example.com" # CSS selector fallback
163
165
  check "I agree"
164
166
  uncheck "Subscribe"
165
167
  attach_file "#upload", "path/to/file.png"
@@ -175,11 +177,18 @@ find("button", text: "Save") # Filter by text
175
177
 
176
178
  #### Assertions & Matchers
177
179
 
180
+ All text and path matchers **automatically wait** for the expected condition to be met (up to `wait_timeout` seconds), making your tests resilient to page transitions and async rendering.
181
+
178
182
  ```ruby
179
- # Check for content
180
- expect(page.body).to include("Success")
181
- expect(find(".alert")).to have_text("Success")
182
- expect(find(".alert")).to have_content("Success") # Alias
183
+ # Check for content (auto-waiting)
184
+ expect(page).to have_text("Success")
185
+ expect(page).to have_content("Success") # Alias for have_text
186
+ expect(find(".alert")).to have_text("Success") # Works on elements too
187
+ expect(page).to have_text(/welcome/i) # Regexp support
188
+
189
+ # Check current path (auto-waiting)
190
+ expect(page).to have_current_path("/dashboard")
191
+ expect(page).to have_current_path(/\/users\/\d+/) # Regexp support
183
192
 
184
193
  # Check for classes
185
194
  expect(find(".alert")).to have_class("success")
@@ -206,6 +215,26 @@ evaluate("document.title") # Execute JS
206
215
  save_screenshot("tmp/screen.png")
207
216
  ```
208
217
 
218
+ ### Auto-Waiting
219
+
220
+ The `have_text`, `have_content`, and `have_current_path` matchers automatically retry until the condition is met or the configured `wait_timeout` expires (default: 5 seconds). This eliminates flaky tests caused by page navigations, redirects, and async rendering.
221
+
222
+ ```ruby
223
+ click_button "Submit"
224
+ # No need for sleep or manual waiting — the matcher will poll until
225
+ # the page transitions and the expected content appears
226
+ expect(page).to have_current_path("/success")
227
+ expect(page).to have_content("Your order has been placed")
228
+ ```
229
+
230
+ For custom waiting logic, use `E2E.wait_until`:
231
+
232
+ ```ruby
233
+ E2E.wait_until(timeout: 10) do
234
+ page.current_url.include?("/ready")
235
+ end
236
+ ```
237
+
209
238
  ### 🔓 Native Access (The Escape Hatch)
210
239
 
211
240
  We believe you shouldn't be limited by the wrapper. You can access the underlying `Playwright::Page` object at any time using `.native`.
@@ -265,6 +294,7 @@ E2E.configure do |config|
265
294
  config.browser_type = :chromium # Options: :chromium (default), :firefox, :webkit
266
295
  config.headless = ENV.fetch("HEADLESS", "true") == "true"
267
296
  config.app = Rails.application # Automatic Rack booting
297
+ config.wait_timeout = 5 # Seconds to wait in auto-waiting matchers (default: 5)
268
298
  end
269
299
  ```
270
300
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- e2e (0.3.1)
4
+ e2e (0.4.0)
5
5
  playwright-ruby-client (>= 1.40.0)
6
6
  rack
7
7
  rackup
@@ -335,7 +335,7 @@ CHECKSUMS
335
335
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
336
336
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
337
337
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
338
- e2e (0.3.1)
338
+ e2e (0.4.0)
339
339
  erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
340
340
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
341
341
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- e2e (0.3.1)
4
+ e2e (0.4.0)
5
5
  playwright-ruby-client (>= 1.40.0)
6
6
  rack
7
7
  rackup
@@ -349,7 +349,7 @@ CHECKSUMS
349
349
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
350
350
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
351
351
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
352
- e2e (0.3.1)
352
+ e2e (0.4.0)
353
353
  erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
354
354
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
355
355
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- e2e (0.3.1)
4
+ e2e (0.4.0)
5
5
  playwright-ruby-client (>= 1.40.0)
6
6
  rack
7
7
  rackup
@@ -343,7 +343,7 @@ CHECKSUMS
343
343
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
344
344
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
345
345
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
346
- e2e (0.3.1)
346
+ e2e (0.4.0)
347
347
  erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
348
348
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
349
349
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- e2e (0.3.1)
4
+ e2e (0.4.0)
5
5
  playwright-ruby-client (>= 1.40.0)
6
6
  rack
7
7
  rackup
@@ -339,7 +339,7 @@ CHECKSUMS
339
339
  diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
340
340
  docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
341
341
  drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
342
- e2e (0.3.1)
342
+ e2e (0.4.0)
343
343
  erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
344
344
  erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
345
345
  globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11
@@ -59,7 +59,21 @@ module E2E
59
59
  end
60
60
 
61
61
  def fill_in(selector, with:)
62
- @page.fill(selector, with)
62
+ # Try to match Capybara's behavior: Label, Placeholder, ID, Name
63
+ chain = @page.get_by_label(selector).or(@page.get_by_placeholder(selector))
64
+
65
+ # Only add ID/Name matching if the selector doesn't contain spaces (valid CSS ID/Name assumption-ish)
66
+ if selector.match?(/^[a-zA-Z0-9_-]+$/)
67
+ chain = chain.or(@page.locator("##{selector}"))
68
+ .or(@page.locator("[name='#{selector}']"))
69
+ end
70
+
71
+ begin
72
+ chain.first.fill(with)
73
+ rescue
74
+ # Fallback to treating it as a direct CSS selector if the above failed
75
+ @page.fill(selector, with)
76
+ end
63
77
  end
64
78
 
65
79
  def check(selector)
@@ -78,6 +92,11 @@ module E2E
78
92
  @page.content
79
93
  end
80
94
 
95
+ # Required for have_content matcher on page object
96
+ def text
97
+ @page.inner_text("body")
98
+ end
99
+
81
100
  def evaluate(script)
82
101
  @page.evaluate(script)
83
102
  end
data/lib/e2e/matchers.rb CHANGED
@@ -16,25 +16,75 @@ if defined?(RSpec::Matchers)
16
16
  end
17
17
 
18
18
  RSpec::Matchers.define :have_text do |expected_text|
19
- match do |element|
20
- if expected_text.is_a?(Regexp)
21
- element.text.match?(expected_text)
22
- else
23
- element.text.include?(expected_text)
19
+ match do |actual|
20
+ @actual_text = nil
21
+
22
+ E2E.wait_until do
23
+ @actual_text = actual.text
24
+ if expected_text.is_a?(Regexp)
25
+ @actual_text.match?(expected_text)
26
+ else
27
+ @actual_text.include?(expected_text)
28
+ end
24
29
  end
25
30
  end
26
31
 
27
- failure_message do |element|
28
- "expected element to have text '#{expected_text}', but it had '#{element.text}'"
32
+ match_when_negated do |actual|
33
+ E2E.wait_until do
34
+ @actual_text = actual.text
35
+ if expected_text.is_a?(Regexp)
36
+ !@actual_text.match?(expected_text)
37
+ else
38
+ !@actual_text.include?(expected_text)
39
+ end
40
+ end
29
41
  end
30
42
 
31
- failure_message_when_negated do |element|
32
- "expected element not to have text '#{expected_text}', but it did"
43
+ failure_message do
44
+ "expected to find text #{expected_text.inspect} in #{@actual_text.inspect}"
45
+ end
46
+
47
+ failure_message_when_negated do
48
+ "expected not to find text #{expected_text.inspect} in #{@actual_text.inspect}"
33
49
  end
34
50
  end
35
51
 
36
52
  RSpec::Matchers.alias_matcher :have_content, :have_text
37
53
 
54
+ RSpec::Matchers.define :have_current_path do |expected_path|
55
+ match do |session|
56
+ @actual_path = nil
57
+
58
+ E2E.wait_until do
59
+ @actual_path = URI.parse(session.current_url).path
60
+ if expected_path.is_a?(Regexp)
61
+ @actual_path.match?(expected_path)
62
+ else
63
+ @actual_path == expected_path
64
+ end
65
+ end
66
+ end
67
+
68
+ match_when_negated do |session|
69
+ E2E.wait_until do
70
+ @actual_path = URI.parse(session.current_url).path
71
+ if expected_path.is_a?(Regexp)
72
+ !@actual_path.match?(expected_path)
73
+ else
74
+ @actual_path != expected_path
75
+ end
76
+ end
77
+ end
78
+
79
+ failure_message do
80
+ "expected current path to be #{expected_path.inspect}, but was #{@actual_path.inspect}"
81
+ end
82
+
83
+ failure_message_when_negated do
84
+ "expected current path not to be #{expected_path.inspect}, but it was"
85
+ end
86
+ end
87
+
38
88
  RSpec::Matchers.define :have_value do |expected_value|
39
89
  match do |element|
40
90
  element.value == expected_value
data/lib/e2e/session.rb CHANGED
@@ -8,7 +8,7 @@ module E2E
8
8
 
9
9
  attr_reader :driver
10
10
 
11
- def_delegators :@driver, :current_url, :click, :click_button, :click_link, :fill_in, :check, :uncheck, :attach_file, :body, :evaluate, :save_screenshot, :native, :pause, :reset!, :quit
11
+ def_delegators :@driver, :current_url, :click, :click_button, :click_link, :fill_in, :check, :uncheck, :attach_file, :body, :text, :evaluate, :save_screenshot, :native, :pause, :reset!, :quit
12
12
 
13
13
  def initialize(driver_name = E2E.config.driver)
14
14
  @driver = initialize_driver(driver_name)
data/lib/e2e/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module E2E
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/e2e.rb CHANGED
@@ -39,14 +39,30 @@ module E2E
39
39
  end
40
40
  end
41
41
 
42
+ # Retry a block until it returns truthy or timeout is reached
43
+ def self.wait_until(timeout: config.wait_timeout, interval: 0.05)
44
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
45
+ loop do
46
+ result = yield
47
+ return result if result
48
+
49
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
50
+ return false
51
+ end
52
+
53
+ sleep interval
54
+ end
55
+ end
56
+
42
57
  class Config
43
- attr_accessor :driver, :headless, :app, :browser_type
58
+ attr_accessor :driver, :headless, :app, :browser_type, :wait_timeout
44
59
 
45
60
  def initialize
46
61
  @driver = :playwright
47
62
  @browser_type = :chromium
48
63
  @headless = ENV.fetch("HEADLESS", "true") == "true"
49
64
  @app = nil
65
+ @wait_timeout = 5
50
66
  end
51
67
  end
52
68
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: e2e
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexey Poimtsev
@@ -238,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
238
  - !ruby/object:Gem::Version
239
239
  version: '0'
240
240
  requirements: []
241
- rubygems_version: 4.0.5
241
+ rubygems_version: 4.0.6
242
242
  specification_version: 4
243
243
  summary: Unified E2E testing framework for Ruby.
244
244
  test_files: []