smartest 0.2.0.alpha2 → 0.2.0.alpha4

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: e0953c11bf61f80c4a310701920e5b6a22727149ed135d86678b3e49b25c8a92
4
- data.tar.gz: d51839a9cb597da3d63a15aa803661946ceb3ae2a6982317ea108797f4203380
3
+ metadata.gz: 944d35bc0a1be2293c8e276d16436693ff7dd6a2d37218f9f65af3b7a47f90f5
4
+ data.tar.gz: edd55fb6442a92e74146eaffc76a3b881463a16b4ce3c72a877734e0f6e36f52
5
5
  SHA512:
6
- metadata.gz: 2e3be2ebdc5b116e6f16e396d4befda198c0063bb4da6e7f487266642d1edd8083bcc4dce9b37746969fdcbd9916424068f043344cc732edf4738f849fb87d9c
7
- data.tar.gz: 0c299af8b8dd05fac80ec1eefaeade8f747f8d2776e87ba6cb684ff84c619be8a21dfd3e33c99fa43d05ee0d40aabc361f86ab1e33af696a5a9ef4c9ee78054d
6
+ metadata.gz: 5406cfcb57b56c71140c98565da884a184b8d14dcd41f8d6f65f218010d5ef7672eee6c3ad874dcc22a2d415097c9f9926f3a22c31ea3f5dafcf6dd00c2c795d
7
+ data.tar.gz: '0666928711e5ec22a5f9d671714597eb9e0a7e98edcf11d334bf72301c42739a8ae71104dff5784fa4728c10c186188e9f5278137ebbf7d22f6e14e745964927'
data/CHANGELOG.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - Support required keyword-argument fixture injection and fixture dependencies.
9
9
  - Support per-test fixture caching and cleanup.
10
10
  - Support suite-scoped fixtures through `suite_fixture`.
11
- - Support `eq`, `include`, `be_nil`, and `raise_error` matchers.
11
+ - Support `eq`, `include`, `start_with`, `end_with`, `be_nil`, `raise_error`, and `change` matchers.
12
12
  - Support custom matcher modules through `use_matcher`.
13
13
  - Generate an opt-in `PredicateMatcher` custom matcher for `be_<predicate>` calls.
14
14
  - Add the `smartest` CLI.
data/README.md CHANGED
@@ -178,6 +178,10 @@ Smartest uses an expectation style:
178
178
  ```ruby
179
179
  expect(actual).to eq(expected)
180
180
  expect(actual).not_to eq(expected)
181
+ expect { action }.to raise_error(ErrorClass)
182
+ expect { action }.to raise_error(/message/)
183
+ expect { action }.to raise_error(ErrorClass, /message/)
184
+ expect { action }.to change { value }
181
185
  ```
182
186
 
183
187
  Examples:
@@ -190,6 +194,26 @@ end
190
194
  test("array") do
191
195
  expect([1, 2, 3]).to include(2)
192
196
  end
197
+
198
+ test("URL") do
199
+ expect("about:blank").to start_with("about:")
200
+ end
201
+
202
+ test("download") do
203
+ expect("screenshot.png").to end_with(".png")
204
+ end
205
+
206
+ test("type") do
207
+ expect("smartest").to be_a(String)
208
+ end
209
+
210
+ test("URL pattern") do
211
+ expect("https://example.test").to match(%r{\Ahttps://})
212
+ end
213
+
214
+ test("events") do
215
+ expect(%i[request close open]).to contain_exactly(:open, :request, :close)
216
+ end
193
217
  ```
194
218
 
195
219
  Supported matchers include:
@@ -197,10 +221,34 @@ Supported matchers include:
197
221
  ```ruby
198
222
  eq(expected)
199
223
  include(expected)
224
+ start_with(prefix, ...)
225
+ end_with(suffix, ...)
226
+ be_a(ClassOrModule)
227
+ be_an(ClassOrModule)
200
228
  be_nil
229
+ match(regexp)
230
+ contain_exactly(item, ...)
231
+ match_array(items)
201
232
  raise_error(ErrorClass)
233
+ raise_error(/message/)
234
+ raise_error(ErrorClass, /message/)
235
+ change { value }
236
+ change { value }.from(before).to(after)
237
+ change { value }.by(delta)
202
238
  ```
203
239
 
240
+ `raise_error` accepts an error class, a message regexp, or both. Use an error
241
+ class to check the raised exception class, a regexp to check the raised
242
+ exception message, or both to require both conditions. No-argument and exact
243
+ string message forms are not supported.
244
+
245
+ `contain_exactly` and `match_array` compare collections without requiring a
246
+ specific order, preserve duplicate counts, and can use matcher objects such as
247
+ `match(/foo/)` or `eq(42)` as expected items.
248
+
249
+ `change` is only supported with `expect { ... }` block expectations and must be
250
+ written with a value block.
251
+
204
252
  Custom matcher modules can be registered from `around_suite` or `around_test`
205
253
  with `use_matcher`. The generated scaffold includes a `PredicateMatcher` custom
206
254
  matcher for `be_<predicate>` calls. See [Matchers](documentation/docs/matchers.md).
data/SMARTEST_DESIGN.md CHANGED
@@ -717,6 +717,26 @@ MVP expectation API:
717
717
  ```ruby
718
718
  expect(actual).to eq(expected)
719
719
  expect(actual).not_to eq(expected)
720
+ expect { action }.to change { value }
721
+ ```
722
+
723
+ Current built-in matchers include:
724
+
725
+ ```ruby
726
+ eq(expected)
727
+ include(expected)
728
+ start_with(prefix, ...)
729
+ end_with(suffix, ...)
730
+ be_a(ClassOrModule)
731
+ be_an(ClassOrModule)
732
+ be_nil
733
+ match(regexp)
734
+ contain_exactly(item, ...)
735
+ match_array(items)
736
+ raise_error(ErrorClass)
737
+ raise_error(/message/)
738
+ raise_error(ErrorClass, /message/)
739
+ change { value }
720
740
  ```
721
741
 
722
742
  Internal model:
@@ -1235,8 +1255,6 @@ Possible future features:
1235
1255
  - custom reporters
1236
1256
  - JSON output
1237
1257
  - richer matchers
1238
- - block expectations
1239
- - `raise_error`
1240
1258
  - file-scoped fixtures
1241
1259
  - parallel execution
1242
1260
  - watch mode
@@ -13,6 +13,12 @@ module Smartest
13
13
  end
14
14
 
15
15
  def not_to(matcher)
16
+ if matcher.respond_to?(:does_not_match?)
17
+ return self if matcher.does_not_match?(@actual)
18
+
19
+ raise AssertionFailed, matcher.negated_failure_message
20
+ end
21
+
16
22
  return self unless matcher.matches?(@actual)
17
23
 
18
24
  raise AssertionFailed, matcher.negated_failure_message
@@ -10,12 +10,47 @@ module Smartest
10
10
  IncludeMatcher.new(expected)
11
11
  end
12
12
 
13
+ def start_with(*prefixes)
14
+ StartWithMatcher.new(*prefixes)
15
+ end
16
+
17
+ def end_with(*suffixes)
18
+ EndWithMatcher.new(*suffixes)
19
+ end
20
+
21
+ def be_a(expected_class)
22
+ BeAKindOfMatcher.new(expected_class)
23
+ end
24
+
25
+ def be_an(expected_class)
26
+ BeAKindOfMatcher.new(expected_class)
27
+ end
28
+
13
29
  def be_nil
14
30
  BeNilMatcher.new
15
31
  end
16
32
 
17
- def raise_error(expected_error = StandardError)
18
- RaiseErrorMatcher.new(expected_error)
33
+ def match(regexp)
34
+ MatchMatcher.new(regexp)
35
+ end
36
+
37
+ def contain_exactly(*expected_items)
38
+ ContainExactlyMatcher.new(expected_items, matcher_name: "contain exactly")
39
+ end
40
+
41
+ def match_array(expected_items)
42
+ ContainExactlyMatcher.new(expected_items, matcher_name: "match array")
43
+ end
44
+
45
+ def raise_error(*expected_error)
46
+ RaiseErrorMatcher.new(*expected_error)
47
+ end
48
+
49
+ def change(*args, &block)
50
+ raise ArgumentError, "change does not support arguments; use change { ... }" if args.any?
51
+ raise ArgumentError, "change requires a block" unless block
52
+
53
+ ChangeMatcher.new(block)
19
54
  end
20
55
  end
21
56
 
@@ -36,6 +71,10 @@ module Smartest
36
71
  def negated_failure_message
37
72
  "expected #{@actual.inspect} not to eq #{@expected.inspect}"
38
73
  end
74
+
75
+ def description
76
+ "eq #{@expected.inspect}"
77
+ end
39
78
  end
40
79
 
41
80
  class IncludeMatcher
@@ -57,6 +96,111 @@ module Smartest
57
96
  def negated_failure_message
58
97
  "expected #{@actual.inspect} not to include #{@expected.inspect}"
59
98
  end
99
+
100
+ def description
101
+ "include #{@expected.inspect}"
102
+ end
103
+ end
104
+
105
+ class StartWithMatcher
106
+ def initialize(*prefixes)
107
+ @prefixes = prefixes
108
+ end
109
+
110
+ def matches?(actual)
111
+ @actual = actual
112
+ actual.start_with?(*@prefixes)
113
+ rescue NoMethodError
114
+ false
115
+ end
116
+
117
+ def failure_message
118
+ "expected #{@actual.inspect} to start with #{expected_description}"
119
+ end
120
+
121
+ def negated_failure_message
122
+ "expected #{@actual.inspect} not to start with #{expected_description}"
123
+ end
124
+
125
+ def description
126
+ "start with #{expected_description}"
127
+ end
128
+
129
+ private
130
+
131
+ def expected_description
132
+ return "no prefixes" if @prefixes.empty?
133
+
134
+ @prefixes.map(&:inspect).join(" or ")
135
+ end
136
+ end
137
+
138
+ class EndWithMatcher
139
+ def initialize(*suffixes)
140
+ @suffixes = suffixes
141
+ end
142
+
143
+ def matches?(actual)
144
+ @actual = actual
145
+ actual.end_with?(*@suffixes)
146
+ rescue NoMethodError
147
+ false
148
+ end
149
+
150
+ def failure_message
151
+ "expected #{@actual.inspect} to end with #{expected_description}"
152
+ end
153
+
154
+ def negated_failure_message
155
+ "expected #{@actual.inspect} not to end with #{expected_description}"
156
+ end
157
+
158
+ def description
159
+ "end with #{expected_description}"
160
+ end
161
+
162
+ private
163
+
164
+ def expected_description
165
+ return "no suffixes" if @suffixes.empty?
166
+
167
+ @suffixes.map(&:inspect).join(" or ")
168
+ end
169
+ end
170
+
171
+ class BeAKindOfMatcher
172
+ def initialize(expected_class)
173
+ @expected_class = expected_class
174
+ end
175
+
176
+ def matches?(actual)
177
+ @actual = actual
178
+ actual.is_a?(@expected_class)
179
+ rescue TypeError
180
+ false
181
+ end
182
+
183
+ def failure_message
184
+ "expected #{@actual.inspect} to be a kind of #{expected_description}, but was #{actual_class_description}"
185
+ end
186
+
187
+ def negated_failure_message
188
+ "expected #{@actual.inspect} not to be a kind of #{expected_description}, but was #{actual_class_description}"
189
+ end
190
+
191
+ def description
192
+ "be a kind of #{expected_description}"
193
+ end
194
+
195
+ private
196
+
197
+ def expected_description
198
+ @expected_class.to_s
199
+ end
200
+
201
+ def actual_class_description
202
+ @actual.class.to_s
203
+ end
60
204
  end
61
205
 
62
206
  class BeNilMatcher
@@ -72,11 +216,164 @@ module Smartest
72
216
  def negated_failure_message
73
217
  "expected #{@actual.inspect} not to be nil"
74
218
  end
219
+
220
+ def description
221
+ "be nil"
222
+ end
223
+ end
224
+
225
+ class MatchMatcher
226
+ def initialize(regexp)
227
+ @regexp = regexp
228
+ end
229
+
230
+ def matches?(actual)
231
+ @actual = actual
232
+ @regexp.match?(actual)
233
+ rescue NoMethodError, TypeError
234
+ false
235
+ end
236
+
237
+ def failure_message
238
+ "expected #{@actual.inspect} to match #{@regexp.inspect}"
239
+ end
240
+
241
+ def negated_failure_message
242
+ "expected #{@actual.inspect} not to match #{@regexp.inspect}"
243
+ end
244
+
245
+ def description
246
+ "match #{@regexp.inspect}"
247
+ end
248
+ end
249
+
250
+ class ContainExactlyMatcher
251
+ def initialize(expected_items, matcher_name:)
252
+ @expected_items = expected_items
253
+ @matcher_name = matcher_name
254
+ reset_result
255
+ end
256
+
257
+ def matches?(actual)
258
+ @actual = actual
259
+ reset_result
260
+ return false unless actual_items?
261
+
262
+ match_items
263
+ @missing_items.empty? && @extra_items.empty?
264
+ end
265
+
266
+ def failure_message
267
+ details = failure_details
268
+ message = "expected #{@actual.inspect} to #{@matcher_name} #{format_expected_items(@expected_items)}"
269
+ details.empty? ? message : "#{message}; #{details.join('; ')}"
270
+ end
271
+
272
+ def negated_failure_message
273
+ "expected #{@actual.inspect} not to #{@matcher_name} #{format_expected_items(@expected_items)}"
274
+ end
275
+
276
+ def description
277
+ "#{@matcher_name} #{format_expected_items(@expected_items)}"
278
+ end
279
+
280
+ private
281
+
282
+ def reset_result
283
+ @actual_items = nil
284
+ @missing_items = @expected_items.dup
285
+ @extra_items = []
286
+ end
287
+
288
+ def actual_items?
289
+ return false unless @actual.respond_to?(:to_a)
290
+
291
+ @actual_items = @actual.to_a
292
+ true
293
+ end
294
+
295
+ def match_items
296
+ adjacency = build_adjacency
297
+ actual_matches = Array.new(@actual_items.length)
298
+ expected_order(adjacency).each do |expected_index|
299
+ assign_expected_item(expected_index, adjacency, actual_matches, [])
300
+ end
301
+
302
+ matched_expected_indexes = actual_matches.compact
303
+ @missing_items = []
304
+ @expected_items.each_with_index do |item, index|
305
+ @missing_items << item unless matched_expected_indexes.include?(index)
306
+ end
307
+ @extra_items = []
308
+ @actual_items.each_with_index do |item, index|
309
+ @extra_items << item if actual_matches[index].nil?
310
+ end
311
+ end
312
+
313
+ def build_adjacency
314
+ @expected_items.map do |expected_item|
315
+ @actual_items.each_index.select do |actual_index|
316
+ expected_item_matches_actual_item?(expected_item, @actual_items[actual_index])
317
+ end
318
+ end
319
+ end
320
+
321
+ def expected_order(adjacency)
322
+ (0...@expected_items.length).sort_by { |index| [adjacency[index].length, index] }
323
+ end
324
+
325
+ def assign_expected_item(expected_index, adjacency, actual_matches, seen_actual_indexes)
326
+ adjacency[expected_index].each do |actual_index|
327
+ next if seen_actual_indexes.include?(actual_index)
328
+
329
+ seen_actual_indexes << actual_index
330
+ if actual_matches[actual_index].nil? ||
331
+ assign_expected_item(actual_matches[actual_index], adjacency, actual_matches, seen_actual_indexes)
332
+ actual_matches[actual_index] = expected_index
333
+ return true
334
+ end
335
+ end
336
+
337
+ false
338
+ end
339
+
340
+ def expected_item_matches_actual_item?(expected_item, actual_item)
341
+ if expected_item.respond_to?(:matches?)
342
+ expected_item.matches?(actual_item)
343
+ else
344
+ actual_item == expected_item
345
+ end
346
+ end
347
+
348
+ def failure_details
349
+ return ["actual did not provide items"] unless @actual_items
350
+
351
+ details = []
352
+ details << "missing: #{format_expected_items(@missing_items)}" unless @missing_items.empty?
353
+ details << "extra: #{format_actual_items(@extra_items)}" unless @extra_items.empty?
354
+ details
355
+ end
356
+
357
+ def format_expected_items(items)
358
+ "[#{items.map { |item| format_expected_item(item) }.join(', ')}]"
359
+ end
360
+
361
+ def format_actual_items(items)
362
+ "[#{items.map(&:inspect).join(', ')}]"
363
+ end
364
+
365
+ def format_expected_item(item)
366
+ return item.description if item.respond_to?(:description)
367
+
368
+ item.inspect
369
+ end
75
370
  end
76
371
 
77
372
  class RaiseErrorMatcher
78
- def initialize(expected_error)
79
- @expected_error = expected_error
373
+ def initialize(*expected_error)
374
+ @expected_type = expected_type_for(expected_error)
375
+ @expected_error_class = expected_error.find { |item| error_class?(item) }
376
+ @expected_message_regexp = expected_error.find { |item| item.is_a?(Regexp) }
80
377
  @actual_error = nil
81
378
  @callable = true
82
379
  end
@@ -92,18 +389,196 @@ module Smartest
92
389
  raise if Smartest.fatal_exception?(error)
93
390
 
94
391
  @actual_error = error
95
- error.is_a?(@expected_error)
392
+ expected_error_matches?(error)
393
+ end
394
+
395
+ def failure_message
396
+ return "expected a block to raise #{expected_description}" unless @callable
397
+ return "expected block to raise #{expected_description}, but nothing was raised" unless @actual_error
398
+
399
+ "expected block to raise #{expected_description}, but raised #{actual_error_description}"
400
+ end
401
+
402
+ def negated_failure_message
403
+ "expected block not to raise #{expected_description}, but raised #{actual_error_description}"
404
+ end
405
+
406
+ private
407
+
408
+ def expected_type_for(expected_error)
409
+ return :class if expected_error.length == 1 && error_class?(expected_error.first)
410
+ return :message_regexp if expected_error.length == 1 && expected_error.first.is_a?(Regexp)
411
+ return :class_and_message_regexp if expected_error.length == 2 &&
412
+ error_class?(expected_error[0]) &&
413
+ expected_error[1].is_a?(Regexp)
414
+
415
+ raise ArgumentError, "raise_error supports an error class, message regexp, or error class and message regexp"
416
+ end
417
+
418
+ def error_class?(value)
419
+ value.is_a?(Class) && value <= Exception
420
+ end
421
+
422
+ def expected_error_matches?(error)
423
+ case @expected_type
424
+ when :class
425
+ error.is_a?(@expected_error_class)
426
+ when :message_regexp
427
+ @expected_message_regexp.match?(error.message)
428
+ when :class_and_message_regexp
429
+ error.is_a?(@expected_error_class) && @expected_message_regexp.match?(error.message)
430
+ end
431
+ end
432
+
433
+ def expected_description
434
+ case @expected_type
435
+ when :class
436
+ @expected_error_class.to_s
437
+ when :message_regexp
438
+ "error with message matching #{@expected_message_regexp.inspect}"
439
+ when :class_and_message_regexp
440
+ "#{@expected_error_class} with message matching #{@expected_message_regexp.inspect}"
441
+ end
442
+ end
443
+
444
+ def actual_error_description
445
+ "#{@actual_error.class}: #{@actual_error.message}"
446
+ end
447
+ end
448
+
449
+ class ChangeMatcher
450
+ UNSET = Object.new
451
+
452
+ def initialize(value_block)
453
+ @value_block = value_block
454
+ @expected_from = UNSET
455
+ @expected_to = UNSET
456
+ @expected_delta = UNSET
457
+ reset_result
458
+ end
459
+
460
+ def from(expected)
461
+ @expected_from = expected
462
+ self
463
+ end
464
+
465
+ def to(expected)
466
+ @expected_to = expected
467
+ self
468
+ end
469
+
470
+ def by(expected_delta)
471
+ @expected_delta = expected_delta
472
+ self
473
+ end
474
+
475
+ def matches?(actual)
476
+ run_change(actual)
477
+ return false unless @callable
478
+
479
+ positive_failures.empty?
480
+ end
481
+
482
+ def does_not_match?(actual)
483
+ run_change(actual)
484
+ return false unless @callable
485
+
486
+ negated_failures.empty?
96
487
  end
97
488
 
98
489
  def failure_message
99
- return "expected a block to raise #{@expected_error}" unless @callable
100
- return "expected block to raise #{@expected_error}, but nothing was raised" unless @actual_error
490
+ return "expected a block to change value" unless @callable
101
491
 
102
- "expected block to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
492
+ "expected value to #{expected_description}, but #{observed_description}#{failed_modifier_description}"
103
493
  end
104
494
 
105
495
  def negated_failure_message
106
- "expected block not to raise #{@expected_error}, but raised #{@actual_error.class}: #{@actual_error.message}"
496
+ return "expected a block not to change value" unless @callable
497
+
498
+ "expected value not to change, but #{observed_description}"
499
+ end
500
+
501
+ private
502
+
503
+ def reset_result
504
+ @callable = true
505
+ @before_value = nil
506
+ @after_value = nil
507
+ @actual_delta = UNSET
508
+ @failed_modifiers = []
509
+ end
510
+
511
+ def run_change(actual)
512
+ reset_result
513
+ @callable = actual.respond_to?(:call)
514
+ return unless @callable
515
+
516
+ @before_value = @value_block.call
517
+ actual.call
518
+ @after_value = @value_block.call
519
+ calculate_delta if delta_expected?
520
+ end
521
+
522
+ def positive_failures
523
+ @failed_modifiers = []
524
+ @failed_modifiers << "change" if !delta_expected? && @before_value == @after_value
525
+ @failed_modifiers << "from(#{@expected_from.inspect})" if from_expected? && @before_value != @expected_from
526
+ @failed_modifiers << "to(#{@expected_to.inspect})" if to_expected? && @after_value != @expected_to
527
+ @failed_modifiers << "by(#{@expected_delta.inspect})" if delta_expected? && @actual_delta != @expected_delta
528
+ @failed_modifiers
529
+ end
530
+
531
+ def negated_failures
532
+ @failed_modifiers = []
533
+ @failed_modifiers << "change" unless @before_value == @after_value
534
+ @failed_modifiers
535
+ end
536
+
537
+ def expected_description
538
+ parts = ["change"]
539
+ parts << "from #{@expected_from.inspect}" if from_expected?
540
+ parts << "to #{@expected_to.inspect}" if to_expected?
541
+ parts << "by #{@expected_delta.inspect}" if delta_expected?
542
+ parts.join(" ")
543
+ end
544
+
545
+ def observed_description
546
+ if delta_expected?
547
+ delta_description = if @actual_delta.equal?(UNSET)
548
+ "could not calculate a numeric difference"
549
+ else
550
+ "changed by #{@actual_delta.inspect}"
551
+ end
552
+
553
+ "#{delta_description} from #{@before_value.inspect} before to #{@after_value.inspect} after"
554
+ else
555
+ "was #{@before_value.inspect} before and #{@after_value.inspect} after"
556
+ end
557
+ end
558
+
559
+ def failed_modifier_description
560
+ failures = positive_failures
561
+ return "" if failures.empty?
562
+
563
+ "; failed modifiers: #{failures.join(', ')}"
564
+ end
565
+
566
+ def calculate_delta
567
+ @actual_delta = @after_value - @before_value
568
+ rescue NoMethodError, TypeError
569
+ @actual_delta = UNSET
570
+ end
571
+
572
+ def from_expected?
573
+ !@expected_from.equal?(UNSET)
574
+ end
575
+
576
+ def to_expected?
577
+ !@expected_to.equal?(UNSET)
578
+ end
579
+
580
+ def delta_expected?
581
+ !@expected_delta.equal?(UNSET)
107
582
  end
108
583
  end
109
584
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smartest
4
- VERSION = "0.2.0.alpha2"
4
+ VERSION = "0.2.0.alpha4"
5
5
  end
@@ -48,6 +48,15 @@ class SelfTestRegisteredFixture < Smartest::Fixture
48
48
  end
49
49
  end
50
50
 
51
+ class SelfTestBaseType; end
52
+ class SelfTestChildType < SelfTestBaseType; end
53
+
54
+ module SelfTestMarkerType; end
55
+
56
+ class SelfTestMarkedType
57
+ include SelfTestMarkerType
58
+ end
59
+
51
60
  around_suite do |suite|
52
61
  use_fixture SelfTestRegisteredFixture
53
62
  suite.run
@@ -172,9 +181,34 @@ test("supports basic matchers") do
172
181
  "matchers",
173
182
  proc do
174
183
  expect([1, 2, 3]).to include(2)
184
+ expect("about:blank").to start_with("about:")
185
+ expect("https://cdn-b.test/assets/app.js").to start_with(
186
+ "https://cdn-a.test",
187
+ "https://cdn-b.test"
188
+ )
189
+ expect("screenshot.png").to end_with(".jpg", ".png")
190
+ expect("https://example.test").not_to start_with("about:")
191
+ expect("report.txt").not_to end_with(".png")
192
+ expect(Object.new).not_to start_with("prefix")
193
+ expect(SelfTestChildType.new).to be_a(SelfTestBaseType)
194
+ expect(SelfTestMarkedType.new).to be_an(SelfTestMarkerType)
175
195
  expect(nil).to be_nil
176
196
  expect("value").not_to be_nil
197
+ expect("https://example.test").to match(%r{\Ahttps://})
198
+ expect("about:blank").not_to match(%r{\Ahttps://})
199
+ expect(%w[request close request]).to contain_exactly("request", "request", "close")
200
+ expect(%i[request close open]).to match_array(%i[open request close])
201
+ expect(["foo", 42]).to contain_exactly(match(/foo/), eq(42))
202
+ expect(["ab", "ac"]).to contain_exactly(match(/a/), "ab")
203
+ expect([nil, false]).to contain_exactly(false, nil)
204
+ expect([:request]).not_to contain_exactly(:request, :request)
177
205
  expect { raise ArgumentError, "bad" }.to raise_error(ArgumentError)
206
+ expect { raise RuntimeError, "request timed out" }.to raise_error(/timed out/)
207
+ expect { raise ArgumentError, "bad input" }.to raise_error(ArgumentError, /bad/)
208
+ expect { raise ArgumentError, "bad" }.not_to raise_error(RuntimeError)
209
+ expect { raise ArgumentError, "bad" }.not_to raise_error(/timed out/)
210
+ expect { raise ArgumentError, "bad" }.not_to raise_error(ArgumentError, /timed out/)
211
+ expect { :ok }.not_to raise_error(/timed out/)
178
212
  end
179
213
  )
180
214
  )
@@ -184,6 +218,253 @@ test("supports basic matchers") do
184
218
  expect(status).to eq(0)
185
219
  end
186
220
 
221
+ test("reports be_a and match matcher failures") do
222
+ suite = Smartest::Suite.new
223
+ suite.tests.add(
224
+ SmartestSelfTest.test_case(
225
+ "bad type",
226
+ proc { expect(nil).to be_a(String) }
227
+ )
228
+ )
229
+ suite.tests.add(
230
+ SmartestSelfTest.test_case(
231
+ "bad negated type",
232
+ proc { expect(SelfTestChildType.new).not_to be_an(SelfTestBaseType) }
233
+ )
234
+ )
235
+ suite.tests.add(
236
+ SmartestSelfTest.test_case(
237
+ "bad regex",
238
+ proc { expect("about:blank").to match(%r{\Ahttps://}) }
239
+ )
240
+ )
241
+ suite.tests.add(
242
+ SmartestSelfTest.test_case(
243
+ "bad negated regex",
244
+ proc { expect("https://example.test").not_to match(%r{\Ahttps://}) }
245
+ )
246
+ )
247
+
248
+ status, output = SmartestSelfTest.run_suite(suite)
249
+
250
+ expect(status).to eq(1)
251
+ expect(output).to include("expected nil to be a kind of String, but was NilClass")
252
+ expect(output).to include("not to be a kind of SelfTestBaseType, but was SelfTestChildType")
253
+ expect(output).to include('expected "about:blank" to match /\\Ahttps:\\/\\//')
254
+ expect(output).to include('expected "https://example.test" not to match /\\Ahttps:\\/\\//')
255
+ end
256
+
257
+ test("reports contain_exactly and match_array matcher failures") do
258
+ suite = Smartest::Suite.new
259
+ suite.tests.add(
260
+ SmartestSelfTest.test_case(
261
+ "bad collection",
262
+ proc { expect(["foo", "baz", 2]).to contain_exactly(match(/foo/), eq(42), "bar") }
263
+ )
264
+ )
265
+ suite.tests.add(
266
+ SmartestSelfTest.test_case(
267
+ "bad duplicate count",
268
+ proc { expect([:request]).to match_array(%i[request request]) }
269
+ )
270
+ )
271
+ suite.tests.add(
272
+ SmartestSelfTest.test_case(
273
+ "bad negated collection",
274
+ proc { expect(%w[b a]).not_to contain_exactly("a", "b") }
275
+ )
276
+ )
277
+
278
+ status, output = SmartestSelfTest.run_suite(suite)
279
+
280
+ expect(status).to eq(1)
281
+ expect(output).to include('expected ["foo", "baz", 2] to contain exactly [match /foo/, eq 42, "bar"]')
282
+ expect(output).to include('missing: [eq 42, "bar"]')
283
+ expect(output).to include('extra: ["baz", 2]')
284
+ expect(output).to include("expected [:request] to match array [:request, :request]")
285
+ expect(output).to include("missing: [:request]")
286
+ expect(output).to include('expected ["b", "a"] not to contain exactly ["a", "b"]')
287
+ end
288
+
289
+ test("reports raise_error matcher failures") do
290
+ suite = Smartest::Suite.new
291
+ suite.tests.add(
292
+ SmartestSelfTest.test_case(
293
+ "nothing raised",
294
+ proc { expect { :ok }.to raise_error(/timeout/) }
295
+ )
296
+ )
297
+ suite.tests.add(
298
+ SmartestSelfTest.test_case(
299
+ "bad message",
300
+ proc { expect { raise RuntimeError, "permission denied" }.to raise_error(/timeout/) }
301
+ )
302
+ )
303
+ suite.tests.add(
304
+ SmartestSelfTest.test_case(
305
+ "bad negated message",
306
+ proc { expect { raise RuntimeError, "timeout after 1s" }.not_to raise_error(/timeout/) }
307
+ )
308
+ )
309
+ suite.tests.add(
310
+ SmartestSelfTest.test_case(
311
+ "bad class and message class",
312
+ proc { expect { raise RuntimeError, "timeout after 1s" }.to raise_error(ArgumentError, /timeout/) }
313
+ )
314
+ )
315
+ suite.tests.add(
316
+ SmartestSelfTest.test_case(
317
+ "bad class and message message",
318
+ proc { expect { raise ArgumentError, "permission denied" }.to raise_error(ArgumentError, /timeout/) }
319
+ )
320
+ )
321
+ suite.tests.add(
322
+ SmartestSelfTest.test_case(
323
+ "bad negated class and message",
324
+ proc { expect { raise ArgumentError, "timeout after 1s" }.not_to raise_error(ArgumentError, /timeout/) }
325
+ )
326
+ )
327
+
328
+ status, output = SmartestSelfTest.run_suite(suite)
329
+
330
+ expect(status).to eq(1)
331
+ expect(output).to include("expected block to raise error with message matching /timeout/, but nothing was raised")
332
+ expect(output).to include(
333
+ "expected block to raise error with message matching /timeout/, but raised RuntimeError: permission denied"
334
+ )
335
+ expect(output).to include(
336
+ "expected block not to raise error with message matching /timeout/, but raised RuntimeError: timeout after 1s"
337
+ )
338
+ expect(output).to include(
339
+ "expected block to raise ArgumentError with message matching /timeout/, but raised RuntimeError: timeout after 1s"
340
+ )
341
+ expect(output).to include(
342
+ "expected block to raise ArgumentError with message matching /timeout/, but raised ArgumentError: permission denied"
343
+ )
344
+ expect(output).to include(
345
+ "expected block not to raise ArgumentError with message matching /timeout/, but raised ArgumentError: timeout after 1s"
346
+ )
347
+ end
348
+
349
+ test("rejects unsupported raise_error argument forms") do
350
+ error = SmartestSelfTest.capture_error(ArgumentError) do
351
+ raise_error
352
+ end
353
+
354
+ expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
355
+
356
+ error = SmartestSelfTest.capture_error(ArgumentError) do
357
+ raise_error("timeout")
358
+ end
359
+
360
+ expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
361
+
362
+ error = SmartestSelfTest.capture_error(ArgumentError) do
363
+ raise_error(String)
364
+ end
365
+
366
+ expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
367
+
368
+ error = SmartestSelfTest.capture_error(ArgumentError) do
369
+ raise_error(RuntimeError, "timeout")
370
+ end
371
+
372
+ expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
373
+
374
+ error = SmartestSelfTest.capture_error(ArgumentError) do
375
+ raise_error(/timeout/, RuntimeError)
376
+ end
377
+
378
+ expect(error.message).to eq("raise_error supports an error class, message regexp, or error class and message regexp")
379
+ end
380
+
381
+ test("reports start_with and end_with matcher failures") do
382
+ suite = Smartest::Suite.new
383
+ suite.tests.add(
384
+ SmartestSelfTest.test_case(
385
+ "bad prefix",
386
+ proc { expect("https://example.test/path").to start_with("about:") }
387
+ )
388
+ )
389
+ suite.tests.add(
390
+ SmartestSelfTest.test_case(
391
+ "bad suffix",
392
+ proc { expect("asset.gif").to end_with(".jpg", ".png") }
393
+ )
394
+ )
395
+ suite.tests.add(
396
+ SmartestSelfTest.test_case(
397
+ "bad negated suffix",
398
+ proc { expect("screenshot.png").not_to end_with(".png") }
399
+ )
400
+ )
401
+
402
+ status, output = SmartestSelfTest.run_suite(suite)
403
+
404
+ expect(status).to eq(1)
405
+ expect(output).to include('expected "https://example.test/path" to start with "about:"')
406
+ expect(output).to include('expected "asset.gif" to end with ".jpg" or ".png"')
407
+ expect(output).to include('expected "screenshot.png" not to end with ".png"')
408
+ end
409
+
410
+ test("supports change matcher for block expectations") do
411
+ value = 0
412
+ action_calls = 0
413
+
414
+ expect {
415
+ action_calls += 1
416
+ value += 2
417
+ }.to change { value }.from(0).to(2).by(2)
418
+
419
+ expect(action_calls).to eq(1)
420
+ expect { value }.not_to change { value }
421
+ end
422
+
423
+ test("reports change matcher failures with before and after values") do
424
+ value = 0
425
+
426
+ error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
427
+ expect { value += 1 }.to change { value }.from(0).to(2).by(2)
428
+ end
429
+
430
+ expect(error.message).to include("0 before")
431
+ expect(error.message).to include("1 after")
432
+ expect(error.message).to include("to(2)")
433
+ expect(error.message).to include("by(2)")
434
+ end
435
+
436
+ test("fails negated change matcher when the value changes") do
437
+ value = 0
438
+
439
+ error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
440
+ expect { value += 1 }.not_to change { value }
441
+ end
442
+
443
+ expect(error.message).to include("expected value not to change")
444
+ expect(error.message).to include("0 before")
445
+ expect(error.message).to include("1 after")
446
+ end
447
+
448
+ test("requires change matcher value and action blocks") do
449
+ error = SmartestSelfTest.capture_error(ArgumentError) do
450
+ change
451
+ end
452
+
453
+ expect(error.message).to eq("change requires a block")
454
+
455
+ error = SmartestSelfTest.capture_error(ArgumentError) do
456
+ change(:value) { :other }
457
+ end
458
+
459
+ expect(error.message).to include("change does not support arguments")
460
+
461
+ error = SmartestSelfTest.capture_error(Smartest::AssertionFailed) do
462
+ expect(:value).to change { :other }
463
+ end
464
+
465
+ expect(error.message).to include("expected a block to change value")
466
+ end
467
+
187
468
  test("registers matcher modules for suite execution contexts") do
188
469
  status_matcher = Class.new do
189
470
  def initialize(expected)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0.alpha2
4
+ version: 0.2.0.alpha4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yusuke Iwaki