zenspec 0.1.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.
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/expectations"
4
+
5
+ module Zenspec
6
+ module Matchers
7
+ module InteractorMatchers
8
+ # Matcher for checking if an interactor succeeds
9
+ #
10
+ # @example
11
+ # expect(result).to succeed
12
+ # expect(result).to succeed.with_data(expected_value)
13
+ # expect(result).to succeed.with_context(:key, value)
14
+ # expect(MyInteractor).to succeed.with_context(:user, user)
15
+ #
16
+ RSpec::Matchers.define :succeed do
17
+ match do |actual|
18
+ @result = get_result(actual)
19
+ return false if @result.failure?
20
+
21
+ return false if @expected_data && @result.data != @expected_data
22
+
23
+ if @context_checks
24
+ @context_checks.all? do |key, value|
25
+ @result.public_send(key) == value
26
+ end
27
+ else
28
+ true
29
+ end
30
+ end
31
+
32
+ chain :with_data do |expected_data|
33
+ @expected_data = expected_data
34
+ end
35
+
36
+ chain :with_context do |key, value|
37
+ @context_checks ||= {}
38
+ @context_checks[key] = value
39
+ end
40
+
41
+ failure_message do
42
+ if @result.failure?
43
+ "expected interactor to succeed, but it failed with errors: #{@result.errors}"
44
+ elsif @expected_data && @result.data != @expected_data
45
+ "expected data to be #{@expected_data.inspect}, got #{@result.data.inspect}"
46
+ elsif @context_checks
47
+ failed_checks = @context_checks.reject { |key, value| @result.public_send(key) == value }
48
+ "expected context to match #{@context_checks.inspect}, but #{failed_checks.inspect} did not match"
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def get_result(actual)
55
+ if actual.is_a?(Class) && actual.ancestors.include?(Interactor)
56
+ actual.call
57
+ else
58
+ actual
59
+ end
60
+ end
61
+ end
62
+
63
+ # Matcher for checking if an interactor fails
64
+ #
65
+ # @example
66
+ # expect(result).to fail_interactor
67
+ # expect(result).to fail_interactor.with_error("invalid_input")
68
+ # expect(result).to fail_interactor.with_errors("error1", "error2")
69
+ # expect(MyInteractor).to fail_interactor.with_error("not_found")
70
+ #
71
+ RSpec::Matchers.define :fail_interactor do
72
+ match do |actual|
73
+ @result = get_result(actual)
74
+ return false if @result.success?
75
+
76
+ if @expected_error_codes && !@expected_error_codes.empty?
77
+ actual_codes = error_codes(@result)
78
+ @expected_error_codes.all? { |code| actual_codes.include?(code) }
79
+ else
80
+ true
81
+ end
82
+ end
83
+
84
+ chain :with_error do |error_code|
85
+ @expected_error_codes ||= []
86
+ @expected_error_codes << error_code
87
+ end
88
+
89
+ chain :with_errors do |*error_codes|
90
+ @expected_error_codes ||= []
91
+ @expected_error_codes.concat(error_codes)
92
+ end
93
+
94
+ failure_message do
95
+ if @result.success?
96
+ "expected interactor to fail, but it succeeded"
97
+ else
98
+ actual_codes = error_codes(@result)
99
+ "expected errors to include #{@expected_error_codes.inspect}, got #{actual_codes.inspect}"
100
+ end
101
+ end
102
+
103
+ def get_result(actual)
104
+ if actual.is_a?(Class) && actual.ancestors.include?(Interactor)
105
+ actual.call
106
+ else
107
+ actual
108
+ end
109
+ end
110
+
111
+ def error_codes(result)
112
+ return [] unless result.errors
113
+
114
+ if result.errors.is_a?(Array)
115
+ result.errors.map { |e| e.is_a?(Hash) ? e[:code] : e.to_s }.compact
116
+ else
117
+ []
118
+ end
119
+ end
120
+ end
121
+
122
+ # Matcher for checking if a result has a specific error code
123
+ #
124
+ # @example
125
+ # expect(result).to have_error_code("invalid_input")
126
+ #
127
+ RSpec::Matchers.define :have_error_code do |expected_code|
128
+ match do |result|
129
+ error_codes(result).include?(expected_code)
130
+ end
131
+
132
+ failure_message do |result|
133
+ "expected errors to include #{expected_code.inspect}, got #{error_codes(result).inspect}"
134
+ end
135
+
136
+ def error_codes(result)
137
+ return [] unless result.errors
138
+
139
+ if result.errors.is_a?(Array)
140
+ result.errors.map { |e| e.is_a?(Hash) ? e[:code] : e.to_s }.compact
141
+ else
142
+ []
143
+ end
144
+ end
145
+ end
146
+
147
+ # Matcher for checking if a result has multiple error codes
148
+ #
149
+ # @example
150
+ # expect(result).to have_error_codes("error1", "error2")
151
+ #
152
+ RSpec::Matchers.define :have_error_codes do |*expected_codes|
153
+ match do |result|
154
+ actual_codes = error_codes(result)
155
+ expected_codes.all? { |code| actual_codes.include?(code) }
156
+ end
157
+
158
+ failure_message do |result|
159
+ "expected errors to include #{expected_codes.inspect}, got #{error_codes(result).inspect}"
160
+ end
161
+
162
+ def error_codes(result)
163
+ return [] unless result.errors
164
+
165
+ if result.errors.is_a?(Array)
166
+ result.errors.map { |e| e.is_a?(Hash) ? e[:code] : e.to_s }.compact
167
+ else
168
+ []
169
+ end
170
+ end
171
+ end
172
+
173
+ # Matcher for checking if context has a specific key/value
174
+ #
175
+ # @example
176
+ # expect(result).to set_context(:data)
177
+ # expect(result).to set_context(:data, expected_value)
178
+ # expect(result).to set_context(:user).to(user)
179
+ #
180
+ RSpec::Matchers.define :set_context do |key, value = nil|
181
+ match do |result|
182
+ return false unless result.respond_to?(key)
183
+
184
+ actual_value = result.public_send(key)
185
+
186
+ if @expected_value
187
+ actual_value == @expected_value
188
+ elsif !value.nil?
189
+ actual_value == value
190
+ else
191
+ !actual_value.nil?
192
+ end
193
+ end
194
+
195
+ chain :to do |expected_value|
196
+ @expected_value = expected_value
197
+ end
198
+
199
+ failure_message do |result|
200
+ if !result.respond_to?(key)
201
+ "expected context to have key #{key.inspect}"
202
+ elsif @expected_value || !value.nil?
203
+ expected = @expected_value || value
204
+ "expected context[#{key.inspect}] to be #{expected.inspect}, got #{result.public_send(key).inspect}"
205
+ else
206
+ "expected context[#{key.inspect}] to be set"
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+ RSpec.configure do |config|
215
+ config.include Zenspec::Matchers::InteractorMatchers
216
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "matchers/interactor_matchers"
4
+ require_relative "matchers/graphql_matchers"
5
+ require_relative "matchers/graphql_type_matchers"
6
+
7
+ module Zenspec
8
+ module Matchers
9
+ # This module serves as a namespace for all Zenspec matchers
10
+ # Individual matcher modules are loaded from their respective files
11
+ end
12
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenspec
4
+ # Docker-style progress loader for displaying progress in terminal
5
+ # Displays progress bars similar to Docker's layer download progress
6
+ #
7
+ # @example Basic usage
8
+ # loader = Zenspec::ProgressLoader.new(total: 10, description: "Running tests")
9
+ # 10.times do |i|
10
+ # loader.update(i + 1)
11
+ # sleep(0.1)
12
+ # end
13
+ # loader.finish
14
+ #
15
+ # @example With custom width
16
+ # loader = Zenspec::ProgressLoader.new(total: 100, width: 50)
17
+ # loader.update(50, description: "Processing files 50/100")
18
+ #
19
+ class ProgressLoader
20
+ attr_reader :current, :total, :width
21
+
22
+ # Default width for the progress bar
23
+ DEFAULT_WIDTH = 40
24
+
25
+ # @param total [Integer] Total number of items to process
26
+ # @param width [Integer] Width of the progress bar in characters
27
+ # @param description [String] Initial description text
28
+ # @param output [IO] Output stream (defaults to STDERR)
29
+ def initialize(total:, width: DEFAULT_WIDTH, description: nil, output: $stderr)
30
+ @total = total
31
+ @width = width
32
+ @current = 0
33
+ @description = description
34
+ @output = output
35
+ @finished = false
36
+ @start_time = Time.now
37
+ end
38
+
39
+ # Update the progress
40
+ # @param current [Integer] Current progress value
41
+ # @param description [String, nil] Optional description to display
42
+ def update(current, description: nil)
43
+ @current = current
44
+ @description = description if description
45
+ render unless @finished
46
+ end
47
+
48
+ # Increment progress by one
49
+ # @param description [String, nil] Optional description to display
50
+ def increment(description: nil)
51
+ update(@current + 1, description: description)
52
+ end
53
+
54
+ # Mark the progress as finished
55
+ # @param description [String, nil] Final description to display
56
+ def finish(description: nil)
57
+ @current = @total
58
+ @description = description if description
59
+ @finished = true
60
+ render
61
+ @output.puts # New line after completion
62
+ end
63
+
64
+ # Calculate current percentage
65
+ # @return [Integer] Percentage (0-100)
66
+ def percentage
67
+ return 100 if @total.zero?
68
+
69
+ ((@current.to_f / @total) * 100).round
70
+ end
71
+
72
+ # Calculate elapsed time
73
+ # @return [Float] Elapsed time in seconds
74
+ def elapsed_time
75
+ Time.now - @start_time
76
+ end
77
+
78
+ # Calculate estimated time remaining
79
+ # @return [Float, nil] Estimated time remaining in seconds, or nil if cannot estimate
80
+ def estimated_time_remaining
81
+ return nil if @current.zero?
82
+
83
+ elapsed = elapsed_time
84
+ rate = @current.to_f / elapsed
85
+ remaining_items = @total - @current
86
+ remaining_items / rate
87
+ end
88
+
89
+ # Format time duration
90
+ # @param seconds [Float] Time in seconds
91
+ # @return [String] Formatted time string
92
+ def format_time(seconds)
93
+ return "0s" if seconds.nil? || seconds.zero?
94
+
95
+ if seconds < 60
96
+ "#{seconds.round}s"
97
+ elsif seconds < 3600
98
+ minutes = (seconds / 60).round
99
+ "#{minutes}m"
100
+ else
101
+ hours = (seconds / 3600).round
102
+ "#{hours}h"
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ # Render the progress bar
109
+ def render
110
+ bar = build_progress_bar
111
+ counter = "#{@current}/#{@total}"
112
+ percent = "#{percentage}%"
113
+ time_info = build_time_info
114
+
115
+ # Build the complete line
116
+ parts = [bar, percent, counter]
117
+ parts << @description if @description
118
+ parts << time_info if time_info && !@finished
119
+
120
+ line = parts.join(" ")
121
+
122
+ # Clear the line and print
123
+ @output.print "\r\e[K#{line}"
124
+ @output.flush
125
+ end
126
+
127
+ # Build the progress bar string
128
+ # @return [String] Progress bar visualization
129
+ def build_progress_bar
130
+ return "[#{' ' * @width}]" if @total.zero?
131
+
132
+ filled_width = ((@current.to_f / @total) * @width).round
133
+ empty_width = @width - filled_width
134
+
135
+ filled = "=" * [filled_width - 1, 0].max
136
+ arrow = filled_width.positive? ? ">" : ""
137
+ empty = " " * empty_width
138
+
139
+ "[#{filled}#{arrow}#{empty}]"
140
+ end
141
+
142
+ # Build time information string
143
+ # @return [String, nil] Time information or nil
144
+ def build_time_info
145
+ elapsed = elapsed_time
146
+ remaining = estimated_time_remaining
147
+
148
+ if remaining && remaining.positive?
149
+ "#{format_time(elapsed)}/#{format_time(remaining + elapsed)}"
150
+ elsif elapsed > 1 # Only show elapsed if more than 1 second
151
+ format_time(elapsed)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenspec
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :zenspec
6
+
7
+ rake_tasks do
8
+ # Add rake tasks if needed in the future
9
+ end
10
+
11
+ initializer "zenspec.configure_rspec" do
12
+ # Automatically configure zenspec matchers and helpers when Rails loads
13
+ if defined?(RSpec)
14
+ RSpec.configure do |config|
15
+ # Include matchers
16
+ config.include Zenspec::Matchers::InteractorMatchers
17
+ config.include Zenspec::Matchers::GraphQLMatchers
18
+ config.include Zenspec::Matchers::GraphQLTypeMatchers
19
+
20
+ # Include helpers
21
+ config.include Zenspec::Helpers::GraphQLHelpers
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure Shoulda Matchers if available
4
+ begin
5
+ require "shoulda/matchers"
6
+
7
+ Shoulda::Matchers.configure do |config|
8
+ config.integrate do |with|
9
+ with.test_framework :rspec
10
+ with.library :active_record if defined?(ActiveRecord)
11
+ with.library :active_model if defined?(ActiveModel)
12
+ with.library :action_controller if defined?(ActionController)
13
+ end
14
+ end
15
+ rescue LoadError
16
+ # Shoulda matchers not available, skip configuration
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Zenspec
4
+ VERSION = "0.1.0"
5
+ end
data/lib/zenspec.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zenspec/version"
4
+ require_relative "zenspec/matchers"
5
+ require_relative "zenspec/helpers"
6
+ require_relative "zenspec/shoulda_config"
7
+ require_relative "zenspec/progress_loader"
8
+
9
+ # Load Railtie if Rails is available
10
+ require_relative "zenspec/railtie" if defined?(Rails::Railtie)
11
+
12
+ module Zenspec
13
+ class Error < StandardError; end
14
+ end
data/sig/zenspec.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Zenspec
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zenspec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wilson Anciro
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: graphql
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.12'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.12'
26
+ - !ruby/object:Gem::Dependency
27
+ name: interactor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: shoulda-matchers
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '6.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '6.0'
68
+ description: A collection of RSpec matchers for testing GraphQL queries and Interactor
69
+ service objects.
70
+ email:
71
+ - konekred@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - CODE_OF_CONDUCT.md
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - examples/progress_loader_demo.rb
82
+ - lib/zenspec.rb
83
+ - lib/zenspec/formatters/progress_bar_formatter.rb
84
+ - lib/zenspec/formatters/progress_formatter.rb
85
+ - lib/zenspec/helpers.rb
86
+ - lib/zenspec/helpers/graphql_helpers.rb
87
+ - lib/zenspec/matchers.rb
88
+ - lib/zenspec/matchers/graphql_matchers.rb
89
+ - lib/zenspec/matchers/graphql_type_matchers.rb
90
+ - lib/zenspec/matchers/interactor_matchers.rb
91
+ - lib/zenspec/progress_loader.rb
92
+ - lib/zenspec/railtie.rb
93
+ - lib/zenspec/shoulda_config.rb
94
+ - lib/zenspec/version.rb
95
+ - sig/zenspec.rbs
96
+ homepage: https://github.com/zyxzen/zenspec
97
+ licenses:
98
+ - MIT
99
+ metadata:
100
+ homepage_uri: https://github.com/zyxzen/zenspec
101
+ source_code_uri: https://github.com/zyxzen/zenspec
102
+ changelog_uri: https://github.com/zyxzen/zenspec/blob/main/CHANGELOG.md
103
+ rubygems_mfa_required: 'true'
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.2.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.6.9
119
+ specification_version: 4
120
+ summary: RSpec matchers for GraphQL, Interactor, and other common testing patterns
121
+ test_files: []