smartest 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +71 -14
- data/SMARTEST_DESIGN.md +3 -0
- data/exe/smartest +13 -0
- data/lib/smartest/fixture.rb +1 -1
- data/lib/smartest/init_rails_generator.rb +242 -0
- data/lib/smartest/rails.rb +73 -0
- data/lib/smartest/simple_stub.rb +37 -36
- data/lib/smartest/version.rb +1 -1
- data/lib/smartest.rb +1 -0
- data/smartest/simple_stub_test.rb +83 -32
- data/smartest/smartest_test.rb +289 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c8e44520f35d8daac5d01122fd2dc2b1f70f0fcbc61c1ff1d2a6c8ce5179a72
|
|
4
|
+
data.tar.gz: 4400e77b387079fa88978ff82d34b492f250fdf6de75aae773911a5b91672169
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b5f7dde5de541b2eec20747164936cee3945076fb985c636d4ae2ea2473e0e2b795b9e5b393800b198e2bae5ae8c6e0892ab173d74e995404a1f4941ce27fd57
|
|
7
|
+
data.tar.gz: 0226224e48d5dfcaa82b9b22fc03ab295cab9fd463f5f0e17bab881d38e9f68eb98946641a45f5b6a13a884d1649c714a6e8aacd86e772b1aa6ec9d82fbf323c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### New Features
|
|
6
|
+
|
|
7
|
+
- Add Rails browser-test integration with `bundle exec smartest --init-rails`,
|
|
8
|
+
generated Playwright fixtures, and `Smartest::Rails::TestServer` loaded by
|
|
9
|
+
explicit `require "smartest/rails"`.
|
|
10
|
+
|
|
11
|
+
### Breaking Changes
|
|
12
|
+
|
|
13
|
+
- Make method stubs process-wide across Fibers and Threads. Remove
|
|
14
|
+
`Smartest::SimpleStub#apply!` and `#reset!`; `#apply` and `#reset` now raise
|
|
15
|
+
when called on an already-applied or not-applied stub object.
|
|
16
|
+
|
|
3
17
|
## 0.5.0
|
|
4
18
|
|
|
5
19
|
### Breaking Changes
|
data/README.md
CHANGED
|
@@ -141,6 +141,9 @@ To create a Smartest test scaffold:
|
|
|
141
141
|
For browser tests:
|
|
142
142
|
bundle exec smartest --init-browser
|
|
143
143
|
|
|
144
|
+
For Rails browser tests:
|
|
145
|
+
bundle exec smartest --init-rails
|
|
146
|
+
|
|
144
147
|
See all commands:
|
|
145
148
|
bundle exec smartest --help
|
|
146
149
|
```
|
|
@@ -190,6 +193,7 @@ bundle exec smartest smartest/user_test.rb
|
|
|
190
193
|
bundle exec smartest smartest/user_test.rb:12
|
|
191
194
|
bundle exec smartest --init
|
|
192
195
|
bundle exec smartest --init-browser
|
|
196
|
+
bundle exec smartest --init-rails
|
|
193
197
|
```
|
|
194
198
|
|
|
195
199
|
Output resembles:
|
|
@@ -235,6 +239,57 @@ Run the generated browser example with:
|
|
|
235
239
|
bundle exec smartest smartest/example_browser_test.rb
|
|
236
240
|
```
|
|
237
241
|
|
|
242
|
+
## Rails browser quick start
|
|
243
|
+
|
|
244
|
+
Initialize a Rails browser-test scaffold:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
bundle exec smartest --init-rails
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The Rails init command creates the normal Smartest helper and matcher files,
|
|
251
|
+
then adds:
|
|
252
|
+
|
|
253
|
+
```text
|
|
254
|
+
smartest/fixtures/rails_system_fixture.rb
|
|
255
|
+
smartest/matchers/playwright_matcher.rb
|
|
256
|
+
smartest/example_rails_system_test.rb
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The generated fixture requires `smartest/rails` and starts
|
|
260
|
+
`Smartest::Rails::TestServer` against `Rails.application` in the same Ruby
|
|
261
|
+
process as the test runner. `Smartest::Rails::TestServer` is only loaded by
|
|
262
|
+
explicitly requiring `smartest/rails`; plain `require "smartest"` does not load
|
|
263
|
+
Puma.
|
|
264
|
+
|
|
265
|
+
This is aimed at local Rails system tests that combine Rails test data, stubs,
|
|
266
|
+
and Playwright browser assertions. It is not a Capybara compatibility layer or
|
|
267
|
+
the main choice for staging / production-like E2E suites; use Node.js
|
|
268
|
+
Playwright Test for that style of E2E testing.
|
|
269
|
+
|
|
270
|
+
The generated `page` fixture uses a per-test Playwright browser context with
|
|
271
|
+
`baseURL` set to the Rails server URL. Set
|
|
272
|
+
`SMARTEST_RAILS_TEST_SERVER_PORT` when you need a fixed port; otherwise the test
|
|
273
|
+
server asks the OS for an available port.
|
|
274
|
+
|
|
275
|
+
For Docker sidecar runs, initialize without installing browser binaries into
|
|
276
|
+
the Rails app container:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
SMARTEST_SKIP_BROWSER_DOWNLOAD=1 bundle exec smartest --init-rails
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
At runtime, set `PLAYWRIGHT_WS_ENDPOINT`,
|
|
283
|
+
`SMARTEST_RAILS_TEST_SERVER_HOST`, `SMARTEST_RAILS_TEST_SERVER_PORT`, and
|
|
284
|
+
`SMARTEST_RAILS_BASE_URL` so the generated fixture connects to the Playwright
|
|
285
|
+
sidecar and gives the browser a Docker-network URL for Rails.
|
|
286
|
+
|
|
287
|
+
Run the generated Rails browser example with:
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
bundle exec smartest smartest/example_rails_system_test.rb
|
|
291
|
+
```
|
|
292
|
+
|
|
238
293
|
## Defining tests
|
|
239
294
|
|
|
240
295
|
Use `test` at the top level:
|
|
@@ -646,7 +701,7 @@ Use simple stub helpers when a fixture needs to temporarily replace a Ruby
|
|
|
646
701
|
method and reset it during teardown:
|
|
647
702
|
|
|
648
703
|
```ruby
|
|
649
|
-
class
|
|
704
|
+
class ApplicationTestFixture < Smartest::Fixture
|
|
650
705
|
fixture :payment_gateway_stub do
|
|
651
706
|
simple_stub_any_instance_of(PaymentGateway, :charge) { :approved }
|
|
652
707
|
end
|
|
@@ -657,7 +712,7 @@ Register the fixture class from `around_suite` before tests request the fixture:
|
|
|
657
712
|
|
|
658
713
|
```ruby
|
|
659
714
|
around_suite do |suite|
|
|
660
|
-
use_fixture
|
|
715
|
+
use_fixture ApplicationTestFixture
|
|
661
716
|
suite.run
|
|
662
717
|
end
|
|
663
718
|
```
|
|
@@ -665,10 +720,10 @@ end
|
|
|
665
720
|
`use_fixture` is available inside `around_suite` or `around_test` blocks, not as
|
|
666
721
|
a top-level method in a test file.
|
|
667
722
|
|
|
668
|
-
The stub affects existing instances and new instances of the target class
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
723
|
+
The stub affects existing instances and new instances of the target class until
|
|
724
|
+
it is reset. Method stubs are shared across Fibers and Threads, including a
|
|
725
|
+
Rails test server running in another thread. Tests can request the fixture to
|
|
726
|
+
make the side effect explicit:
|
|
672
727
|
|
|
673
728
|
```ruby
|
|
674
729
|
test("checkout succeeds") do |payment_gateway_stub:|
|
|
@@ -680,20 +735,22 @@ Use `simple_stub(Time, :now) { fixed_time }` for singleton methods such as class
|
|
|
680
735
|
methods.
|
|
681
736
|
|
|
682
737
|
Use `with_stub_const("AppConfig::PAYMENT_PROVIDER", "fake") { ... }` for
|
|
683
|
-
constants in test bodies, `around_test`, or `around_suite`.
|
|
684
|
-
|
|
738
|
+
constants in test bodies, `around_test`, or `around_suite`.
|
|
739
|
+
|
|
740
|
+
Smartest stubs are process-wide state. They are intended for serial test
|
|
741
|
+
execution and for cases like a Rails test server thread serving the current
|
|
742
|
+
test. They do not provide isolation for multi-threaded parallel test execution:
|
|
743
|
+
one test can observe or reset another test's method or constant stub.
|
|
685
744
|
|
|
686
745
|
The method stub helpers call `Smartest::SimpleStub` internally, apply the stub,
|
|
687
746
|
register `on_teardown { stub.reset }`, and return the stub object.
|
|
688
747
|
`with_stub_const` records the previous constant value, replaces it, yields to
|
|
689
748
|
the block, and restores or removes the constant with `ensure`.
|
|
690
749
|
|
|
691
|
-
`Smartest::SimpleStub#apply`
|
|
692
|
-
`
|
|
693
|
-
`Smartest::SimpleStub::
|
|
694
|
-
|
|
695
|
-
`Smartest::SimpleStub::NotAppliedError` when it is not active there. See
|
|
696
|
-
[Stubs](documentation/docs/stubs.md).
|
|
750
|
+
`Smartest::SimpleStub#apply` raises
|
|
751
|
+
`Smartest::SimpleStub::AlreadyAppliedError` when the same stub object is already
|
|
752
|
+
applied. `#reset` raises `Smartest::SimpleStub::NotAppliedError` when that stub
|
|
753
|
+
object is not applied. See [Stubs](documentation/docs/stubs.md).
|
|
697
754
|
|
|
698
755
|
## Logged-in client example
|
|
699
756
|
|
data/SMARTEST_DESIGN.md
CHANGED
data/exe/smartest
CHANGED
|
@@ -28,6 +28,9 @@ usage = <<~USAGE
|
|
|
28
28
|
bundle exec smartest --init-browser
|
|
29
29
|
Generate a Playwright browser-test scaffold
|
|
30
30
|
|
|
31
|
+
bundle exec smartest --init-rails
|
|
32
|
+
Generate a Rails Playwright browser-test scaffold
|
|
33
|
+
|
|
31
34
|
Options:
|
|
32
35
|
--profile N
|
|
33
36
|
Print the N slowest tests. Defaults to 5.
|
|
@@ -48,6 +51,9 @@ missing_smartest_directory_message = <<~MESSAGE
|
|
|
48
51
|
For browser tests:
|
|
49
52
|
bundle exec smartest --init-browser
|
|
50
53
|
|
|
54
|
+
For Rails browser tests:
|
|
55
|
+
bundle exec smartest --init-rails
|
|
56
|
+
|
|
51
57
|
See all commands:
|
|
52
58
|
bundle exec smartest --help
|
|
53
59
|
MESSAGE
|
|
@@ -75,6 +81,11 @@ begin
|
|
|
75
81
|
exit Smartest::InitBrowserGenerator.new.run
|
|
76
82
|
end
|
|
77
83
|
|
|
84
|
+
if ARGV.include?("--init-rails")
|
|
85
|
+
command = :init_rails
|
|
86
|
+
exit Smartest::InitRailsGenerator.new.run
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
Smartest.disable_autorun!
|
|
79
90
|
Smartest.install_dsl!
|
|
80
91
|
test_load_path = File.expand_path("smartest", Dir.pwd)
|
|
@@ -103,6 +114,8 @@ rescue Exception => error
|
|
|
103
114
|
"Error initializing Smartest:"
|
|
104
115
|
when :init_browser
|
|
105
116
|
"Error initializing Smartest browser scaffold:"
|
|
117
|
+
when :init_rails
|
|
118
|
+
"Error initializing Smartest Rails browser scaffold:"
|
|
106
119
|
else
|
|
107
120
|
"Error loading tests:"
|
|
108
121
|
end
|
data/lib/smartest/fixture.rb
CHANGED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Smartest
|
|
6
|
+
class InitRailsGenerator
|
|
7
|
+
RAILS_SYSTEM_FIXTURE = <<~RUBY
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
require 'smartest/rails'
|
|
11
|
+
require "playwright"
|
|
12
|
+
|
|
13
|
+
class RailsSystemTestFixture < Smartest::Fixture
|
|
14
|
+
suite_fixture :rails_server do
|
|
15
|
+
# Set the environment before loading config/environment so the test
|
|
16
|
+
# server cannot boot against the development database by default.
|
|
17
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
18
|
+
ENV["RACK_ENV"] ||= ENV["RAILS_ENV"]
|
|
19
|
+
require_relative "../../config/environment"
|
|
20
|
+
|
|
21
|
+
server = Smartest::Rails::TestServer.new(
|
|
22
|
+
app: Rails.application,
|
|
23
|
+
host: ENV["SMARTEST_RAILS_TEST_SERVER_HOST"],
|
|
24
|
+
port: ENV["SMARTEST_RAILS_TEST_SERVER_PORT"],
|
|
25
|
+
)
|
|
26
|
+
server.start
|
|
27
|
+
server.wait_for_ready
|
|
28
|
+
|
|
29
|
+
on_teardown do
|
|
30
|
+
server.stop
|
|
31
|
+
server.wait_for_stopped
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
server
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
suite_fixture :base_url do |rails_server:|
|
|
38
|
+
ENV.fetch("SMARTEST_RAILS_BASE_URL", rails_server.base_url)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
suite_fixture :browser do
|
|
42
|
+
ws_endpoint = ENV["PLAYWRIGHT_WS_ENDPOINT"]
|
|
43
|
+
|
|
44
|
+
if ws_endpoint && !ws_endpoint.empty?
|
|
45
|
+
playwright_execution = Playwright.connect_to_browser_server(
|
|
46
|
+
ws_endpoint,
|
|
47
|
+
browser_type: selected_browser_type.to_s,
|
|
48
|
+
)
|
|
49
|
+
on_teardown { playwright_execution.stop }
|
|
50
|
+
|
|
51
|
+
playwright_execution.browser
|
|
52
|
+
else
|
|
53
|
+
playwright_execution = Playwright.create(
|
|
54
|
+
playwright_cli_executable_path: ENV.fetch(
|
|
55
|
+
"PLAYWRIGHT_CLI_EXECUTABLE_PATH",
|
|
56
|
+
"./node_modules/.bin/playwright",
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
on_teardown { playwright_execution.stop }
|
|
60
|
+
|
|
61
|
+
playwright = playwright_execution.playwright
|
|
62
|
+
browser = playwright.public_send(selected_browser_type).launch(**browser_launch_options)
|
|
63
|
+
on_teardown { browser.close }
|
|
64
|
+
browser
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
fixture :browser_context do |base_url:, browser:|
|
|
69
|
+
context = browser.new_context(baseURL: base_url)
|
|
70
|
+
on_teardown { context.close }
|
|
71
|
+
context
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
fixture :page do |browser_context:|
|
|
75
|
+
page = browser_context.new_page
|
|
76
|
+
on_teardown { page.close }
|
|
77
|
+
page
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def selected_browser_type
|
|
83
|
+
case ENV.fetch("BROWSER", "chromium")
|
|
84
|
+
when "firefox"
|
|
85
|
+
:firefox
|
|
86
|
+
when "webkit"
|
|
87
|
+
:webkit
|
|
88
|
+
else
|
|
89
|
+
:chromium
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def browser_launch_options
|
|
94
|
+
launch_options = {}
|
|
95
|
+
launch_options[:headless] = !%w[0 false].include?(ENV.fetch("HEADLESS", "true"))
|
|
96
|
+
if (slow_mo = ENV.fetch("SLOW_MO", "0").to_i) > 0
|
|
97
|
+
launch_options[:slowMo] = slow_mo
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
launch_options
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
RUBY
|
|
104
|
+
|
|
105
|
+
PLAYWRIGHT_MATCHER = <<~RUBY
|
|
106
|
+
# frozen_string_literal: true
|
|
107
|
+
|
|
108
|
+
require "playwright"
|
|
109
|
+
require "playwright/test"
|
|
110
|
+
|
|
111
|
+
module PlaywrightMatcher
|
|
112
|
+
include Playwright::Test::Matchers
|
|
113
|
+
end
|
|
114
|
+
RUBY
|
|
115
|
+
|
|
116
|
+
EXAMPLE_RAILS_SYSTEM_TEST = <<~RUBY
|
|
117
|
+
# frozen_string_literal: true
|
|
118
|
+
|
|
119
|
+
require "test_helper"
|
|
120
|
+
|
|
121
|
+
test("loads the Rails application") do |page:|
|
|
122
|
+
response = page.goto("/")
|
|
123
|
+
|
|
124
|
+
expect(response.status).to be_between(200, 599)
|
|
125
|
+
end
|
|
126
|
+
RUBY
|
|
127
|
+
|
|
128
|
+
def initialize(root: Dir.pwd, output: $stdout, command_runner: nil)
|
|
129
|
+
@root = root
|
|
130
|
+
@output = output
|
|
131
|
+
@command_runner = command_runner || method(:run_system_command)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def run
|
|
135
|
+
Smartest::InitGenerator.new(
|
|
136
|
+
root: @root,
|
|
137
|
+
output: @output,
|
|
138
|
+
files: smartest_files,
|
|
139
|
+
final_message: nil
|
|
140
|
+
).run
|
|
141
|
+
create_file("smartest/fixtures/rails_system_fixture.rb", RAILS_SYSTEM_FIXTURE)
|
|
142
|
+
create_file("smartest/matchers/playwright_matcher.rb", PLAYWRIGHT_MATCHER)
|
|
143
|
+
update_test_helper
|
|
144
|
+
update_gemfile
|
|
145
|
+
install_dependencies
|
|
146
|
+
@output.puts
|
|
147
|
+
@output.puts "Run your Rails browser test suite with: bundle exec smartest smartest/example_rails_system_test.rb"
|
|
148
|
+
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def smartest_files
|
|
155
|
+
Smartest::InitGenerator::FILES.merge("smartest/example_rails_system_test.rb" => EXAMPLE_RAILS_SYSTEM_TEST)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_file(path, contents)
|
|
159
|
+
absolute_path = File.join(@root, path)
|
|
160
|
+
|
|
161
|
+
if File.exist?(absolute_path)
|
|
162
|
+
@output.puts "exist #{path}"
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
FileUtils.mkdir_p(File.dirname(absolute_path))
|
|
167
|
+
File.write(absolute_path, contents)
|
|
168
|
+
@output.puts "create #{path}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def update_test_helper
|
|
172
|
+
path = File.join(@root, "smartest/test_helper.rb")
|
|
173
|
+
contents = File.read(path)
|
|
174
|
+
updated = ensure_rails_registered(contents)
|
|
175
|
+
|
|
176
|
+
return if updated == contents
|
|
177
|
+
|
|
178
|
+
File.write(path, updated)
|
|
179
|
+
@output.puts "update smartest/test_helper.rb"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def ensure_rails_registered(contents)
|
|
183
|
+
missing_lines = []
|
|
184
|
+
missing_lines << " use_fixture RailsSystemTestFixture\n" unless contents.include?("use_fixture RailsSystemTestFixture")
|
|
185
|
+
missing_lines << " use_matcher PlaywrightMatcher\n" unless contents.include?("use_matcher PlaywrightMatcher")
|
|
186
|
+
return contents if missing_lines.empty?
|
|
187
|
+
|
|
188
|
+
if contents.include?("use_matcher PredicateMatcher")
|
|
189
|
+
contents.sub(/^(\s*use_matcher PredicateMatcher\n)/) do
|
|
190
|
+
"#{Regexp.last_match(1)}#{missing_lines.join}"
|
|
191
|
+
end
|
|
192
|
+
else
|
|
193
|
+
"#{contents.chomp}\n\naround_suite do |suite|\n#{missing_lines.join} suite.run\nend\n"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def update_gemfile
|
|
198
|
+
path = File.join(@root, "Gemfile")
|
|
199
|
+
exists = File.exist?(path)
|
|
200
|
+
contents = exists ? File.read(path) : "source \"https://rubygems.org\"\n"
|
|
201
|
+
|
|
202
|
+
if contents.match?(/gem ["']playwright-ruby-client["']/)
|
|
203
|
+
@output.puts "exist Gemfile playwright-ruby-client"
|
|
204
|
+
return
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
separator = contents.end_with?("\n") ? "" : "\n"
|
|
208
|
+
updated = "#{contents}#{separator}\ngem \"playwright-ruby-client\", group: :test\n"
|
|
209
|
+
File.write(path, updated)
|
|
210
|
+
@output.puts(exists ? "update Gemfile" : "create Gemfile")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def install_dependencies
|
|
214
|
+
install_commands.each do |command|
|
|
215
|
+
@output.puts "run #{command.join(" ")}"
|
|
216
|
+
next if @command_runner.call(command, chdir: @root)
|
|
217
|
+
|
|
218
|
+
raise "command failed: #{command.join(" ")}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
if skip_browser_download?
|
|
222
|
+
@output.puts "skip ./node_modules/.bin/playwright install (SMARTEST_SKIP_BROWSER_DOWNLOAD=1)"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def install_commands
|
|
227
|
+
commands = [["bundle", "install"]]
|
|
228
|
+
commands << ["npm", "init", "--yes"] unless File.exist?(File.join(@root, "package.json"))
|
|
229
|
+
commands << ["npm", "install", "playwright", "--save-dev"]
|
|
230
|
+
commands << ["./node_modules/.bin/playwright", "install"] unless skip_browser_download?
|
|
231
|
+
commands
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def skip_browser_download?
|
|
235
|
+
%w[1 true].include?(ENV.fetch("SMARTEST_SKIP_BROWSER_DOWNLOAD", "false"))
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def run_system_command(command, chdir:)
|
|
239
|
+
system(*command, chdir: chdir)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "puma"
|
|
5
|
+
require "timeout"
|
|
6
|
+
|
|
7
|
+
require_relative "../smartest"
|
|
8
|
+
|
|
9
|
+
module Smartest
|
|
10
|
+
module Rails
|
|
11
|
+
class TestServer
|
|
12
|
+
DEFAULT_READY_TIMEOUT = 10
|
|
13
|
+
|
|
14
|
+
attr_reader :host, :port
|
|
15
|
+
|
|
16
|
+
def initialize(app:, host: nil, port: nil)
|
|
17
|
+
@app = app
|
|
18
|
+
@host = host || "127.0.0.1"
|
|
19
|
+
@requested_port = port ? port.to_i : 0
|
|
20
|
+
@server = Puma::Server.new(@app)
|
|
21
|
+
@port = bind_tcp_listener
|
|
22
|
+
@thread = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def start
|
|
26
|
+
@thread ||= @server.run
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def stop
|
|
30
|
+
@server.stop
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def wait_for_ready(timeout: DEFAULT_READY_TIMEOUT)
|
|
34
|
+
Timeout.timeout(timeout) do
|
|
35
|
+
sleep 0.05 until responsive?
|
|
36
|
+
end
|
|
37
|
+
rescue Timeout::Error
|
|
38
|
+
raise "Rails test server did not become ready at #{base_url} within #{timeout} seconds"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def wait_for_stopped(timeout: DEFAULT_READY_TIMEOUT)
|
|
42
|
+
return unless @thread
|
|
43
|
+
return if @thread.join(timeout)
|
|
44
|
+
|
|
45
|
+
raise "Rails test server did not stop within #{timeout} seconds"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def base_url
|
|
49
|
+
"http://#{host}:#{port}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def bind_tcp_listener
|
|
55
|
+
listener = @server.add_tcp_listener(@host, @requested_port)
|
|
56
|
+
bound_port = listener.respond_to?(:addr) ? listener.addr[1] : @requested_port
|
|
57
|
+
|
|
58
|
+
return bound_port if bound_port && bound_port.positive?
|
|
59
|
+
|
|
60
|
+
raise "Rails test server could not determine bound port"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def responsive?
|
|
64
|
+
Net::HTTP.start(host, port, open_timeout: 0.2, read_timeout: 0.2) do |http|
|
|
65
|
+
http.head("/")
|
|
66
|
+
end
|
|
67
|
+
true
|
|
68
|
+
rescue EOFError, IOError, Net::OpenTimeout, Net::ReadTimeout, SocketError, SystemCallError
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/smartest/simple_stub.rb
CHANGED
|
@@ -4,22 +4,40 @@ require "digest"
|
|
|
4
4
|
|
|
5
5
|
module Smartest
|
|
6
6
|
class SimpleStub
|
|
7
|
-
|
|
7
|
+
StubEntry = Struct.new(:owner, :implementation)
|
|
8
8
|
|
|
9
9
|
class AlreadyAppliedError < Smartest::Error; end
|
|
10
10
|
class NotAppliedError < Smartest::Error; end
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def implementation_for(klass_key, method_name)
|
|
14
|
-
|
|
14
|
+
stub_registry_mutex.synchronize do
|
|
15
|
+
active_stubs.fetch(stub_key(klass_key, method_name), nil)&.last&.implementation
|
|
16
|
+
end
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
+
def activate_stub(stub, stub_key, implementation)
|
|
20
|
+
stub_registry_mutex.synchronize do
|
|
21
|
+
stack = active_stubs[stub_key] ||= []
|
|
22
|
+
return false if stack.any? { |entry| entry.owner.equal?(stub) }
|
|
23
|
+
|
|
24
|
+
stack << StubEntry.new(stub, implementation)
|
|
25
|
+
true
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
|
|
21
|
-
def
|
|
22
|
-
|
|
29
|
+
def deactivate_stub(stub, stub_key)
|
|
30
|
+
stub_registry_mutex.synchronize do
|
|
31
|
+
stack = active_stubs.fetch(stub_key, nil)
|
|
32
|
+
return false unless stack
|
|
33
|
+
|
|
34
|
+
index = stack.index { |entry| entry.owner.equal?(stub) }
|
|
35
|
+
return false unless index
|
|
36
|
+
|
|
37
|
+
stack.delete_at(index)
|
|
38
|
+
active_stubs.delete(stub_key) if stack.empty?
|
|
39
|
+
true
|
|
40
|
+
end
|
|
23
41
|
end
|
|
24
42
|
|
|
25
43
|
def ensure_dispatcher_method(klass, klass_key, method_name)
|
|
@@ -45,10 +63,6 @@ module Smartest
|
|
|
45
63
|
[klass_key, method_name]
|
|
46
64
|
end
|
|
47
65
|
|
|
48
|
-
def current_stubs
|
|
49
|
-
Thread.current[STORAGE_KEY]
|
|
50
|
-
end
|
|
51
|
-
|
|
52
66
|
def call_implementation(receiver, implementation, args, kwargs, block)
|
|
53
67
|
return call_implementation_with_block(receiver, implementation, args, kwargs, block) if block
|
|
54
68
|
|
|
@@ -110,6 +124,14 @@ module Smartest
|
|
|
110
124
|
def call_mutex
|
|
111
125
|
@call_mutex ||= Mutex.new
|
|
112
126
|
end
|
|
127
|
+
|
|
128
|
+
def active_stubs
|
|
129
|
+
@active_stubs ||= {}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def stub_registry_mutex
|
|
133
|
+
@stub_registry_mutex ||= Mutex.new
|
|
134
|
+
end
|
|
113
135
|
end
|
|
114
136
|
|
|
115
137
|
def initialize(klass, method_name, &implementation)
|
|
@@ -122,26 +144,10 @@ module Smartest
|
|
|
122
144
|
end
|
|
123
145
|
|
|
124
146
|
def apply
|
|
125
|
-
return if stub_defined?
|
|
126
|
-
|
|
127
|
-
apply_stub
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def apply!
|
|
131
|
-
raise AlreadyAppliedError, "stub for #{@klass}##{@method_name} is already applied" if stub_defined?
|
|
132
|
-
|
|
133
147
|
apply_stub
|
|
134
148
|
end
|
|
135
149
|
|
|
136
150
|
def reset
|
|
137
|
-
return unless stub_defined?
|
|
138
|
-
|
|
139
|
-
reset_stub
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def reset!
|
|
143
|
-
raise NotAppliedError, "stub for #{@klass}##{@method_name} is not applied" unless stub_defined?
|
|
144
|
-
|
|
145
151
|
reset_stub
|
|
146
152
|
end
|
|
147
153
|
|
|
@@ -151,16 +157,15 @@ module Smartest
|
|
|
151
157
|
raise ArgumentError, "block must be given for applying stub" unless @implementation
|
|
152
158
|
|
|
153
159
|
self.class.ensure_dispatcher_method(@klass, klass_key, @method_name)
|
|
154
|
-
|
|
160
|
+
return self if self.class.activate_stub(self, stub_key, @implementation)
|
|
161
|
+
|
|
162
|
+
raise AlreadyAppliedError, "stub for #{@klass}##{@method_name} is already applied"
|
|
155
163
|
end
|
|
156
164
|
|
|
157
165
|
def reset_stub
|
|
158
|
-
|
|
159
|
-
self.class.clear_active_stubs_if_empty
|
|
160
|
-
end
|
|
166
|
+
return self if self.class.deactivate_stub(self, stub_key)
|
|
161
167
|
|
|
162
|
-
|
|
163
|
-
self.class.active_stubs
|
|
168
|
+
raise NotAppliedError, "stub for #{@klass}##{@method_name} is not applied"
|
|
164
169
|
end
|
|
165
170
|
|
|
166
171
|
def stub_key
|
|
@@ -170,9 +175,5 @@ module Smartest
|
|
|
170
175
|
def klass_key
|
|
171
176
|
@klass_key ||= Digest::SHA256.hexdigest(@klass.object_id.to_s)
|
|
172
177
|
end
|
|
173
|
-
|
|
174
|
-
def stub_defined?
|
|
175
|
-
self.class.current_stubs&.key?(stub_key)
|
|
176
|
-
end
|
|
177
178
|
end
|
|
178
179
|
end
|
data/lib/smartest/version.rb
CHANGED
data/lib/smartest.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "smartest/reporter"
|
|
|
28
28
|
require_relative "smartest/runner"
|
|
29
29
|
require_relative "smartest/init_generator"
|
|
30
30
|
require_relative "smartest/init_browser_generator"
|
|
31
|
+
require_relative "smartest/init_rails_generator"
|
|
31
32
|
require_relative "smartest/cli_arguments"
|
|
32
33
|
|
|
33
34
|
module Smartest
|
|
@@ -71,7 +71,7 @@ test("simple stub stubs instance methods until reset") do
|
|
|
71
71
|
existing = SimpleStubSelfTestSubject.new("Alice")
|
|
72
72
|
stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed" }
|
|
73
73
|
|
|
74
|
-
stub.apply
|
|
74
|
+
stub.apply
|
|
75
75
|
|
|
76
76
|
begin
|
|
77
77
|
expect(existing.name).to eq("stubbed")
|
|
@@ -84,13 +84,23 @@ test("simple stub stubs instance methods until reset") do
|
|
|
84
84
|
expect(SimpleStubSelfTestSubject.new("Bob").name).to eq("original Bob")
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
-
test("simple stub
|
|
88
|
-
Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :greeting)
|
|
87
|
+
test("simple stub reset requires the applied stub object") do
|
|
88
|
+
stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :greeting) do |prefix|
|
|
89
|
+
"#{prefix}, stubbed"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
stub.apply
|
|
89
93
|
|
|
90
94
|
begin
|
|
91
95
|
expect(SimpleStubSelfTestSubject.new("Alice").greeting("Hi")).to eq("Hi, stubbed")
|
|
96
|
+
|
|
97
|
+
error = SimpleStubSelfTest.capture_error(Smartest::SimpleStub::NotAppliedError) do
|
|
98
|
+
Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :greeting).reset
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
expect(error.message).to eq("stub for SimpleStubSelfTestSubject#greeting is not applied")
|
|
92
102
|
ensure
|
|
93
|
-
|
|
103
|
+
stub.reset
|
|
94
104
|
end
|
|
95
105
|
|
|
96
106
|
expect(SimpleStubSelfTestSubject.new("Alice").greeting("Hi")).to eq("Hi, Alice")
|
|
@@ -127,6 +137,39 @@ test("simple_stub_any_instance_of applies and resets from fixture teardown") do
|
|
|
127
137
|
expect(status).to eq(0)
|
|
128
138
|
end
|
|
129
139
|
|
|
140
|
+
test("test-scoped method stubs restore suite-scoped method stubs") do
|
|
141
|
+
fixture_class = Class.new(Smartest::Fixture) do
|
|
142
|
+
suite_fixture :suite_name_stub do
|
|
143
|
+
simple_stub_any_instance_of(SimpleStubSelfTestSubject, :name) { "suite #{@name}" }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
fixture :test_name_stub do |suite_name_stub:|
|
|
147
|
+
expect(suite_name_stub).to be_a(Smartest::SimpleStub)
|
|
148
|
+
simple_stub_any_instance_of(SimpleStubSelfTestSubject, :name) { "test #{@name}" }
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
suite = Smartest::Suite.new
|
|
153
|
+
suite.fixture_classes.add(fixture_class)
|
|
154
|
+
suite.tests.add(
|
|
155
|
+
SimpleStubSelfTest.test_case(
|
|
156
|
+
"uses test scoped override",
|
|
157
|
+
proc { |test_name_stub:| expect(SimpleStubSelfTestSubject.new("Alice").name).to eq("test Alice") }
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
suite.tests.add(
|
|
161
|
+
SimpleStubSelfTest.test_case(
|
|
162
|
+
"sees suite scoped stub after override teardown",
|
|
163
|
+
proc { |suite_name_stub:| expect(SimpleStubSelfTestSubject.new("Alice").name).to eq("suite Alice") }
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
status, = SimpleStubSelfTest.run_suite(suite)
|
|
168
|
+
|
|
169
|
+
expect(status).to eq(0)
|
|
170
|
+
expect(SimpleStubSelfTestSubject.new("Alice").name).to eq("original Alice")
|
|
171
|
+
end
|
|
172
|
+
|
|
130
173
|
test("simple_stub applies and resets singleton methods from fixture teardown") do
|
|
131
174
|
fixture_class = Class.new(Smartest::Fixture) do
|
|
132
175
|
fixture :stubbed_time do
|
|
@@ -261,7 +304,7 @@ test("simple stub preserves receiver self and method blocks") do
|
|
|
261
304
|
block.call("#{prefix}, stubbed #{@name}")
|
|
262
305
|
end
|
|
263
306
|
|
|
264
|
-
stub.apply
|
|
307
|
+
stub.apply
|
|
265
308
|
|
|
266
309
|
begin
|
|
267
310
|
result = subject.yielding_greeting("Hi") { |message| message.upcase }
|
|
@@ -273,16 +316,16 @@ test("simple stub preserves receiver self and method blocks") do
|
|
|
273
316
|
expect(subject.yielding_greeting("Hi") { |message| message }).to eq("Hi, Alice")
|
|
274
317
|
end
|
|
275
318
|
|
|
276
|
-
test("simple stub is
|
|
319
|
+
test("simple stub is shared with fibers") do
|
|
277
320
|
subject = SimpleStubSelfTestSubject.new("Alice")
|
|
278
321
|
stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed #{@name}" }
|
|
279
322
|
|
|
280
|
-
stub.apply
|
|
323
|
+
stub.apply
|
|
281
324
|
|
|
282
325
|
begin
|
|
283
326
|
expect(subject.name).to eq("stubbed Alice")
|
|
284
327
|
Fiber.new do
|
|
285
|
-
expect(subject.name).to eq("
|
|
328
|
+
expect(subject.name).to eq("stubbed Alice")
|
|
286
329
|
end.resume
|
|
287
330
|
ensure
|
|
288
331
|
stub.reset
|
|
@@ -291,52 +334,62 @@ test("simple stub is scoped to the current fiber") do
|
|
|
291
334
|
expect(subject.name).to eq("original Alice")
|
|
292
335
|
end
|
|
293
336
|
|
|
294
|
-
test("simple stub
|
|
337
|
+
test("simple stub is shared with threads") do
|
|
295
338
|
subject = SimpleStubSelfTestSubject.new("Alice")
|
|
296
339
|
queue = Queue.new
|
|
297
|
-
|
|
340
|
+
stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed #{@name}" }
|
|
298
341
|
|
|
299
|
-
|
|
342
|
+
stub.apply
|
|
300
343
|
|
|
301
344
|
thread = Thread.new do
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
begin
|
|
306
|
-
queue << subject.name
|
|
307
|
-
rescue Exception => error
|
|
308
|
-
queue << error
|
|
309
|
-
ensure
|
|
310
|
-
thread_stub.reset
|
|
311
|
-
end
|
|
345
|
+
queue << subject.name
|
|
346
|
+
rescue Exception => error
|
|
347
|
+
queue << error
|
|
312
348
|
end
|
|
313
349
|
|
|
314
350
|
begin
|
|
315
|
-
expect(subject.name).to eq("
|
|
351
|
+
expect(subject.name).to eq("stubbed Alice")
|
|
316
352
|
|
|
317
353
|
thread_result = queue.pop
|
|
318
354
|
raise thread_result if thread_result.is_a?(Exception)
|
|
319
355
|
|
|
320
|
-
expect(thread_result).to eq("
|
|
356
|
+
expect(thread_result).to eq("stubbed Alice")
|
|
321
357
|
thread.join
|
|
322
|
-
expect(subject.name).to eq("main Alice")
|
|
323
358
|
ensure
|
|
324
|
-
|
|
359
|
+
stub.reset
|
|
325
360
|
thread.kill if thread.alive?
|
|
326
361
|
end
|
|
327
362
|
|
|
328
363
|
expect(subject.name).to eq("original Alice")
|
|
329
364
|
end
|
|
330
365
|
|
|
331
|
-
test("simple stub
|
|
366
|
+
test("simple stub restores previous implementation when nested stubs reset") do
|
|
367
|
+
subject = SimpleStubSelfTestSubject.new("Alice")
|
|
368
|
+
outer_stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "outer #{@name}" }
|
|
369
|
+
inner_stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "inner #{@name}" }
|
|
370
|
+
|
|
371
|
+
outer_stub.apply
|
|
372
|
+
inner_stub.apply
|
|
373
|
+
|
|
374
|
+
begin
|
|
375
|
+
expect(subject.name).to eq("inner Alice")
|
|
376
|
+
inner_stub.reset
|
|
377
|
+
expect(subject.name).to eq("outer Alice")
|
|
378
|
+
ensure
|
|
379
|
+
outer_stub.reset
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
expect(subject.name).to eq("original Alice")
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
test("simple stub apply and reset are strict") do
|
|
332
386
|
stub = Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name) { "stubbed" }
|
|
333
387
|
|
|
334
|
-
stub.apply
|
|
335
388
|
stub.apply
|
|
336
389
|
|
|
337
390
|
begin
|
|
338
391
|
error = SimpleStubSelfTest.capture_error(Smartest::SimpleStub::AlreadyAppliedError) do
|
|
339
|
-
|
|
392
|
+
stub.apply
|
|
340
393
|
end
|
|
341
394
|
|
|
342
395
|
expect(error.message).to eq("stub for SimpleStubSelfTestSubject#name is already applied")
|
|
@@ -344,10 +397,8 @@ test("simple stub supports safe and strict apply reset APIs") do
|
|
|
344
397
|
stub.reset
|
|
345
398
|
end
|
|
346
399
|
|
|
347
|
-
stub.reset
|
|
348
|
-
|
|
349
400
|
error = SimpleStubSelfTest.capture_error(Smartest::SimpleStub::NotAppliedError) do
|
|
350
|
-
stub.reset
|
|
401
|
+
stub.reset
|
|
351
402
|
end
|
|
352
403
|
|
|
353
404
|
expect(error.message).to eq("stub for SimpleStubSelfTestSubject#name is not applied")
|
|
@@ -367,7 +418,7 @@ test("simple stub validates constructor arguments and apply block") do
|
|
|
367
418
|
expect(error.message).to eq("method name must be a Symbol.")
|
|
368
419
|
|
|
369
420
|
error = SimpleStubSelfTest.capture_error(ArgumentError) do
|
|
370
|
-
Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name).apply
|
|
421
|
+
Smartest::SimpleStub.new(SimpleStubSelfTestSubject, :name).apply
|
|
371
422
|
end
|
|
372
423
|
|
|
373
424
|
expect(error.message).to eq("block must be given for applying stub")
|
data/smartest/smartest_test.rb
CHANGED
|
@@ -9,6 +9,8 @@ require "open3"
|
|
|
9
9
|
require "tmpdir"
|
|
10
10
|
|
|
11
11
|
module SmartestSelfTest
|
|
12
|
+
ENV_MISSING = Object.new.freeze
|
|
13
|
+
|
|
12
14
|
module_function
|
|
13
15
|
|
|
14
16
|
def test_case(name, block)
|
|
@@ -40,6 +42,20 @@ module SmartestSelfTest
|
|
|
40
42
|
else
|
|
41
43
|
raise Smartest::AssertionFailed, "expected #{expected_error}, but nothing was raised"
|
|
42
44
|
end
|
|
45
|
+
|
|
46
|
+
def with_env(values)
|
|
47
|
+
previous_values = {}
|
|
48
|
+
values.each do |key, value|
|
|
49
|
+
previous_values[key] = ENV.fetch(key, ENV_MISSING)
|
|
50
|
+
value.nil? ? ENV.delete(key) : ENV[key] = value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
yield
|
|
54
|
+
ensure
|
|
55
|
+
previous_values.each do |key, value|
|
|
56
|
+
value.equal?(ENV_MISSING) ? ENV.delete(key) : ENV[key] = value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
43
59
|
end
|
|
44
60
|
|
|
45
61
|
class SelfTestRegisteredFixture < Smartest::Fixture
|
|
@@ -1723,6 +1739,7 @@ test("cli prints help") do
|
|
|
1723
1739
|
expect(stdout).not_to include("Run a browser test file")
|
|
1724
1740
|
expect(stdout).to include("bundle exec smartest --init")
|
|
1725
1741
|
expect(stdout).to include("bundle exec smartest --init-browser")
|
|
1742
|
+
expect(stdout).to include("bundle exec smartest --init-rails")
|
|
1726
1743
|
expect(stdout).to include("--profile N")
|
|
1727
1744
|
expect(stdout).to include("smartest/**/*_test.rb")
|
|
1728
1745
|
end
|
|
@@ -1741,6 +1758,7 @@ test("cli explains how to initialize when default smartest directory is missing"
|
|
|
1741
1758
|
expect(stdout).to include("No smartest/ directory found.")
|
|
1742
1759
|
expect(stdout).to include("bundle exec smartest --init")
|
|
1743
1760
|
expect(stdout).to include("bundle exec smartest --init-browser")
|
|
1761
|
+
expect(stdout).to include("bundle exec smartest --init-rails")
|
|
1744
1762
|
expect(stdout).to include("bundle exec smartest --help")
|
|
1745
1763
|
end
|
|
1746
1764
|
end
|
|
@@ -2198,3 +2216,274 @@ test("cli browser init generator skips npm init when package.json already exists
|
|
|
2198
2216
|
expect(output.string).not_to include("npm init --yes")
|
|
2199
2217
|
end
|
|
2200
2218
|
end
|
|
2219
|
+
|
|
2220
|
+
test("smartest rails helper is loaded only by explicit require") do
|
|
2221
|
+
lib_path = File.expand_path("../lib", __dir__)
|
|
2222
|
+
stdout, stderr, status = Open3.capture3(
|
|
2223
|
+
{ "RUBYLIB" => lib_path },
|
|
2224
|
+
"ruby",
|
|
2225
|
+
"-e",
|
|
2226
|
+
'require "smartest"; puts Smartest.const_defined?(:Rails, false)'
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2229
|
+
expect(status.success?).to eq(true)
|
|
2230
|
+
expect(stderr).to eq("")
|
|
2231
|
+
expect(stdout).to eq("false\n")
|
|
2232
|
+
|
|
2233
|
+
Dir.mktmpdir do |dir|
|
|
2234
|
+
File.write(File.join(dir, "puma.rb"), "module Puma\n class Server\n end\nend\n")
|
|
2235
|
+
|
|
2236
|
+
stdout, stderr, status = Open3.capture3(
|
|
2237
|
+
{ "RUBYLIB" => "#{dir}:#{lib_path}" },
|
|
2238
|
+
"ruby",
|
|
2239
|
+
"-e",
|
|
2240
|
+
'require "smartest/rails"; puts Smartest::Rails.const_defined?(:TestServer, false)'
|
|
2241
|
+
)
|
|
2242
|
+
|
|
2243
|
+
expect(status.success?).to eq(true)
|
|
2244
|
+
expect(stderr).to eq("")
|
|
2245
|
+
expect(stdout).to eq("true\n")
|
|
2246
|
+
end
|
|
2247
|
+
end
|
|
2248
|
+
|
|
2249
|
+
test("smartest rails test server wraps puma with explicit arguments and lifecycle") do
|
|
2250
|
+
lib_path = File.expand_path("../lib", __dir__)
|
|
2251
|
+
|
|
2252
|
+
Dir.mktmpdir do |dir|
|
|
2253
|
+
File.write(File.join(dir, "puma.rb"), <<~RUBY)
|
|
2254
|
+
module Puma
|
|
2255
|
+
class Listener
|
|
2256
|
+
def initialize(port)
|
|
2257
|
+
@port = port
|
|
2258
|
+
end
|
|
2259
|
+
|
|
2260
|
+
def addr
|
|
2261
|
+
["AF_INET", @port]
|
|
2262
|
+
end
|
|
2263
|
+
end
|
|
2264
|
+
|
|
2265
|
+
class Server
|
|
2266
|
+
attr_reader :stopped
|
|
2267
|
+
|
|
2268
|
+
def initialize(app)
|
|
2269
|
+
@app = app
|
|
2270
|
+
end
|
|
2271
|
+
|
|
2272
|
+
def add_tcp_listener(_host, port)
|
|
2273
|
+
Listener.new(port.zero? ? 4321 : port)
|
|
2274
|
+
end
|
|
2275
|
+
|
|
2276
|
+
def run
|
|
2277
|
+
Thread.new {}
|
|
2278
|
+
end
|
|
2279
|
+
|
|
2280
|
+
def stop
|
|
2281
|
+
@stopped = true
|
|
2282
|
+
end
|
|
2283
|
+
end
|
|
2284
|
+
end
|
|
2285
|
+
RUBY
|
|
2286
|
+
|
|
2287
|
+
stdout, stderr, status = Open3.capture3(
|
|
2288
|
+
{
|
|
2289
|
+
"RUBYLIB" => "#{dir}:#{lib_path}",
|
|
2290
|
+
"SMARTEST_RAILS_TEST_SERVER_HOST" => "0.0.0.0",
|
|
2291
|
+
"SMARTEST_RAILS_TEST_SERVER_PORT" => "9876",
|
|
2292
|
+
"SMARTEST_RAILS_BASE_URL" => "http://app:9876"
|
|
2293
|
+
},
|
|
2294
|
+
"ruby",
|
|
2295
|
+
"-e",
|
|
2296
|
+
<<~'RUBY'
|
|
2297
|
+
require "smartest/rails"
|
|
2298
|
+
|
|
2299
|
+
server = Smartest::Rails::TestServer.new(app: Object.new, port: "4567")
|
|
2300
|
+
default_port_server = Smartest::Rails::TestServer.new(app: Object.new)
|
|
2301
|
+
thread = server.start
|
|
2302
|
+
server.stop
|
|
2303
|
+
server.wait_for_stopped
|
|
2304
|
+
|
|
2305
|
+
puts server.base_url
|
|
2306
|
+
puts default_port_server.base_url
|
|
2307
|
+
puts thread.is_a?(Thread)
|
|
2308
|
+
RUBY
|
|
2309
|
+
)
|
|
2310
|
+
|
|
2311
|
+
expect(status.success?).to eq(true)
|
|
2312
|
+
expect(stderr).to eq("")
|
|
2313
|
+
expect(stdout).to eq("http://127.0.0.1:4567\nhttp://127.0.0.1:4321\ntrue\n")
|
|
2314
|
+
end
|
|
2315
|
+
end
|
|
2316
|
+
|
|
2317
|
+
test("cli rails init generator creates Rails browser scaffold and installation commands") do
|
|
2318
|
+
Dir.mktmpdir do |dir|
|
|
2319
|
+
File.write(File.join(dir, "Gemfile"), <<~RUBY)
|
|
2320
|
+
source "https://rubygems.org"
|
|
2321
|
+
|
|
2322
|
+
gem "rails"
|
|
2323
|
+
gem "smartest"
|
|
2324
|
+
RUBY
|
|
2325
|
+
|
|
2326
|
+
commands = []
|
|
2327
|
+
output = StringIO.new
|
|
2328
|
+
generator = Smartest::InitRailsGenerator.new(
|
|
2329
|
+
root: dir,
|
|
2330
|
+
output: output,
|
|
2331
|
+
command_runner: ->(command, chdir:) {
|
|
2332
|
+
commands << [command, chdir]
|
|
2333
|
+
true
|
|
2334
|
+
}
|
|
2335
|
+
)
|
|
2336
|
+
|
|
2337
|
+
status = generator.run
|
|
2338
|
+
|
|
2339
|
+
expect(status).to eq(0)
|
|
2340
|
+
rails_fixture = File.read(File.join(dir, "smartest/fixtures/rails_system_fixture.rb"))
|
|
2341
|
+
expect(rails_fixture).to include("require 'smartest/rails'")
|
|
2342
|
+
expect(rails_fixture).to include("class RailsSystemTestFixture < Smartest::Fixture")
|
|
2343
|
+
expect(rails_fixture).to include("suite_fixture :rails_server")
|
|
2344
|
+
expect(rails_fixture).to include("server cannot boot against the development database")
|
|
2345
|
+
expect(rails_fixture).to include('require_relative "../../config/environment"')
|
|
2346
|
+
expect(rails_fixture).to include("Smartest::Rails::TestServer.new(")
|
|
2347
|
+
expect(rails_fixture).to include('host: ENV["SMARTEST_RAILS_TEST_SERVER_HOST"]')
|
|
2348
|
+
expect(rails_fixture).not_to include("bind_host:")
|
|
2349
|
+
expect(rails_fixture).to include('port: ENV["SMARTEST_RAILS_TEST_SERVER_PORT"]')
|
|
2350
|
+
expect(rails_fixture).not_to include("public_base_url")
|
|
2351
|
+
expect(rails_fixture).not_to include("SmartestRailsTestServer")
|
|
2352
|
+
expect(rails_fixture).not_to include("Puma::Server")
|
|
2353
|
+
expect(rails_fixture).to include("suite_fixture :base_url")
|
|
2354
|
+
expect(rails_fixture).to include('ENV.fetch("SMARTEST_RAILS_BASE_URL", rails_server.base_url)')
|
|
2355
|
+
expect(rails_fixture).not_to include("rails_test_server_port")
|
|
2356
|
+
expect(rails_fixture).not_to include("SMARTEST_RAILS_PORT")
|
|
2357
|
+
expect(rails_fixture).to include('ws_endpoint = ENV["PLAYWRIGHT_WS_ENDPOINT"]')
|
|
2358
|
+
expect(rails_fixture).not_to include("suite_fixture :playwright_execution")
|
|
2359
|
+
expect(rails_fixture).not_to include("suite_fixture :playwright do")
|
|
2360
|
+
expect(rails_fixture).not_to include("Playwright.connect_to_playwright_server")
|
|
2361
|
+
expect(rails_fixture).not_to include("rescue NotImplementedError")
|
|
2362
|
+
expect(rails_fixture).to include("playwright_execution = Playwright.connect_to_browser_server(")
|
|
2363
|
+
expect(rails_fixture).to include("browser_type: selected_browser_type.to_s")
|
|
2364
|
+
expect(rails_fixture).to include("playwright_execution.browser")
|
|
2365
|
+
expect(rails_fixture).to include('ENV.fetch(')
|
|
2366
|
+
expect(rails_fixture).to include('"PLAYWRIGHT_CLI_EXECUTABLE_PATH"')
|
|
2367
|
+
expect(rails_fixture).to include("suite_fixture :browser do")
|
|
2368
|
+
expect(rails_fixture).to include("playwright_execution = Playwright.create(")
|
|
2369
|
+
expect(rails_fixture).to include("on_teardown { playwright_execution.stop }")
|
|
2370
|
+
expect(rails_fixture).to include("playwright = playwright_execution.playwright")
|
|
2371
|
+
expect(rails_fixture).to include("playwright.public_send(selected_browser_type).launch(**browser_launch_options)")
|
|
2372
|
+
expect(rails_fixture).to include("def selected_browser_type")
|
|
2373
|
+
expect(rails_fixture).to include("def browser_launch_options")
|
|
2374
|
+
expect(rails_fixture).to include("fixture :browser_context do |base_url:, browser:|")
|
|
2375
|
+
expect(rails_fixture).to include("context = browser.new_context(baseURL: base_url)")
|
|
2376
|
+
expect(rails_fixture).to include("fixture :page do |browser_context:|")
|
|
2377
|
+
|
|
2378
|
+
example_test = File.read(File.join(dir, "smartest/example_rails_system_test.rb"))
|
|
2379
|
+
expect(example_test).to include('test("loads the Rails application") do |page:|')
|
|
2380
|
+
expect(example_test).to include('response = page.goto("/")')
|
|
2381
|
+
expect(example_test).to include("expect(response.status).to be_between(200, 599)")
|
|
2382
|
+
|
|
2383
|
+
helper_contents = File.read(File.join(dir, "smartest/test_helper.rb"))
|
|
2384
|
+
expect(helper_contents).to include("use_matcher PredicateMatcher\n use_fixture RailsSystemTestFixture\n use_matcher PlaywrightMatcher\n suite.run")
|
|
2385
|
+
expect(helper_contents).not_to include("Smartest::SimpleStub")
|
|
2386
|
+
|
|
2387
|
+
gemfile_contents = File.read(File.join(dir, "Gemfile"))
|
|
2388
|
+
expect(gemfile_contents).to include('gem "playwright-ruby-client", group: :test')
|
|
2389
|
+
expect(commands).to eq(
|
|
2390
|
+
[
|
|
2391
|
+
[["bundle", "install"], dir],
|
|
2392
|
+
[["npm", "init", "--yes"], dir],
|
|
2393
|
+
[["npm", "install", "playwright", "--save-dev"], dir],
|
|
2394
|
+
[["./node_modules/.bin/playwright", "install"], dir]
|
|
2395
|
+
]
|
|
2396
|
+
)
|
|
2397
|
+
expect(output.string).to include("Run your Rails browser test suite with: bundle exec smartest smartest/example_rails_system_test.rb")
|
|
2398
|
+
end
|
|
2399
|
+
end
|
|
2400
|
+
|
|
2401
|
+
test("cli rails init generator skips browser install when requested by environment") do
|
|
2402
|
+
SmartestSelfTest.with_env("SMARTEST_SKIP_BROWSER_DOWNLOAD" => "1") do
|
|
2403
|
+
Dir.mktmpdir do |dir|
|
|
2404
|
+
File.write(File.join(dir, "Gemfile"), <<~RUBY)
|
|
2405
|
+
source "https://rubygems.org"
|
|
2406
|
+
|
|
2407
|
+
gem "rails"
|
|
2408
|
+
gem "smartest"
|
|
2409
|
+
RUBY
|
|
2410
|
+
|
|
2411
|
+
commands = []
|
|
2412
|
+
output = StringIO.new
|
|
2413
|
+
generator = Smartest::InitRailsGenerator.new(
|
|
2414
|
+
root: dir,
|
|
2415
|
+
output: output,
|
|
2416
|
+
command_runner: ->(command, chdir:) {
|
|
2417
|
+
commands << [command, chdir]
|
|
2418
|
+
true
|
|
2419
|
+
}
|
|
2420
|
+
)
|
|
2421
|
+
|
|
2422
|
+
status = generator.run
|
|
2423
|
+
|
|
2424
|
+
expect(status).to eq(0)
|
|
2425
|
+
expect(commands).to eq(
|
|
2426
|
+
[
|
|
2427
|
+
[["bundle", "install"], dir],
|
|
2428
|
+
[["npm", "init", "--yes"], dir],
|
|
2429
|
+
[["npm", "install", "playwright", "--save-dev"], dir]
|
|
2430
|
+
]
|
|
2431
|
+
)
|
|
2432
|
+
expect(output.string).to include("skip ./node_modules/.bin/playwright install (SMARTEST_SKIP_BROWSER_DOWNLOAD=1)")
|
|
2433
|
+
expect(File.exist?(File.join(dir, "smartest/fixtures/rails_system_fixture.rb"))).to eq(true)
|
|
2434
|
+
expect(File.exist?(File.join(dir, "smartest/example_rails_system_test.rb"))).to eq(true)
|
|
2435
|
+
end
|
|
2436
|
+
end
|
|
2437
|
+
end
|
|
2438
|
+
|
|
2439
|
+
test("cli rails init generator skips duplicate registration and dependencies") do
|
|
2440
|
+
Dir.mktmpdir do |dir|
|
|
2441
|
+
FileUtils.mkdir_p(File.join(dir, "smartest/fixtures"))
|
|
2442
|
+
FileUtils.mkdir_p(File.join(dir, "smartest/matchers"))
|
|
2443
|
+
File.write(File.join(dir, "package.json"), "{\n \"name\": \"existing\"\n}\n")
|
|
2444
|
+
File.write(File.join(dir, "Gemfile"), <<~RUBY)
|
|
2445
|
+
source "https://rubygems.org"
|
|
2446
|
+
|
|
2447
|
+
gem "smartest"
|
|
2448
|
+
gem "playwright-ruby-client", group: :test
|
|
2449
|
+
RUBY
|
|
2450
|
+
File.write(File.join(dir, "smartest/test_helper.rb"), <<~RUBY)
|
|
2451
|
+
require "smartest/autorun"
|
|
2452
|
+
|
|
2453
|
+
around_suite do |suite|
|
|
2454
|
+
use_matcher PredicateMatcher
|
|
2455
|
+
use_fixture RailsSystemTestFixture
|
|
2456
|
+
use_matcher PlaywrightMatcher
|
|
2457
|
+
suite.run
|
|
2458
|
+
end
|
|
2459
|
+
RUBY
|
|
2460
|
+
|
|
2461
|
+
commands = []
|
|
2462
|
+
output = StringIO.new
|
|
2463
|
+
generator = Smartest::InitRailsGenerator.new(
|
|
2464
|
+
root: dir,
|
|
2465
|
+
output: output,
|
|
2466
|
+
command_runner: ->(command, chdir:) {
|
|
2467
|
+
commands << [command, chdir]
|
|
2468
|
+
true
|
|
2469
|
+
}
|
|
2470
|
+
)
|
|
2471
|
+
|
|
2472
|
+
status = generator.run
|
|
2473
|
+
|
|
2474
|
+
expect(status).to eq(0)
|
|
2475
|
+
helper_contents = File.read(File.join(dir, "smartest/test_helper.rb"))
|
|
2476
|
+
expect(helper_contents.scan("use_fixture RailsSystemTestFixture").length).to eq(1)
|
|
2477
|
+
expect(helper_contents.scan("use_matcher PlaywrightMatcher").length).to eq(1)
|
|
2478
|
+
expect(helper_contents).not_to include("Smartest::SimpleStub")
|
|
2479
|
+
expect(commands).to eq(
|
|
2480
|
+
[
|
|
2481
|
+
[["bundle", "install"], dir],
|
|
2482
|
+
[["npm", "install", "playwright", "--save-dev"], dir],
|
|
2483
|
+
[["./node_modules/.bin/playwright", "install"], dir]
|
|
2484
|
+
]
|
|
2485
|
+
)
|
|
2486
|
+
expect(output.string).to include("exist Gemfile playwright-ruby-client")
|
|
2487
|
+
expect(output.string).not_to include("npm init --yes")
|
|
2488
|
+
end
|
|
2489
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smartest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Iwaki
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -62,9 +62,11 @@ files:
|
|
|
62
62
|
- lib/smartest/hook_contexts.rb
|
|
63
63
|
- lib/smartest/init_browser_generator.rb
|
|
64
64
|
- lib/smartest/init_generator.rb
|
|
65
|
+
- lib/smartest/init_rails_generator.rb
|
|
65
66
|
- lib/smartest/matcher_registry.rb
|
|
66
67
|
- lib/smartest/matchers.rb
|
|
67
68
|
- lib/smartest/parameter_extractor.rb
|
|
69
|
+
- lib/smartest/rails.rb
|
|
68
70
|
- lib/smartest/reporter.rb
|
|
69
71
|
- lib/smartest/runner.rb
|
|
70
72
|
- lib/smartest/simple_stub.rb
|