rspec-xlsx_matchers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 35beb1261a867c92fa8f9c4b27c260e70fb6c98491c5a1ec3944e4ac79baa090
4
+ data.tar.gz: 6c9abea563485512965144e52410175c2209a09551c60c5e896ea8110bc4d623
5
+ SHA512:
6
+ metadata.gz: f5ab021b80d51fe3430f554ab04234074d4884bc9e0dd90e090e0d6718a9f5a432a771a520cba1b70a0375b099ba6d311fac9331e4d485da06d151691a877dd9
7
+ data.tar.gz: f08ca7a76cf5b4e781dfd6feda7054f7a63b7b7710611e5df8354f327da47de7f0c5dccdcfea5f91e8eb6e63209f30724117c5fe5c43c8266dd837679d8a2eda
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,31 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-rake
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 3.0
7
+ NewCops: enable
8
+
9
+ Style/StringLiterals:
10
+ Enabled: true
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/StringLiteralsInInterpolation:
14
+ Enabled: true
15
+ EnforcedStyle: double_quotes
16
+
17
+ Layout/LineLength:
18
+ Max: 120
19
+
20
+ Naming/PredicateName:
21
+ NamePrefix:
22
+ - is_
23
+
24
+ RSpec/MultipleExpectations:
25
+ Enabled: false
26
+
27
+ RSpec/ExampleLength:
28
+ Enabled: false
29
+
30
+ RSpec/SubjectDeclaration:
31
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-05-29
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 dreeven-oss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # RSpec::XlsxMatchers
2
+
3
+ Provides rspec matchers for testing xlsx data with `roo` or `caxlsx` gems.
4
+
5
+ ## Optional dependencies
6
+
7
+ - [roo](https://github.com/roo-rb/roo)
8
+ - [caxlsx](https://github.com/caxlsx/caxlsx)
9
+
10
+ ## Configure
11
+
12
+ add `config.include RSpec::XlsxMatchers` to your RSpec.configure declaration
13
+
14
+ ```ruby
15
+ # spec_helper.rb
16
+ RSpec.configure do |config|
17
+ config.include RSpec::XlsxMatchers
18
+ # ...
19
+ end
20
+ ```
21
+
22
+ ## Matchers
23
+
24
+ ### have_excel_sheets
25
+
26
+ Tests that xlsx data contains specific sheets (by name or zero-based index)
27
+
28
+ `subject` must be one of the following types:
29
+ - `String`: file path to an excel file (requires roo gem)
30
+ - `File`: file path to an excel file (requires roo gem)
31
+ - `Roo::Excelx`: excel file loaded with roo (requires roo gem)
32
+ - `Axlsx::Package`: (requires caxlsx gem)
33
+ - `Axlsx::Workbook`: (requires caxlsx gem)
34
+
35
+
36
+ #### examples
37
+ ```ruby
38
+ it "has a sheet Sheet1" do
39
+ expect(subject).to have_excel_sheets("Sheet1")
40
+ end
41
+
42
+ it "has sheets Sheet1 and Sheet2" do
43
+ expect(subject).to have_excel_sheets(%w[Sheet1 Sheet2])
44
+ end
45
+
46
+ it "has at least 3 sheets" do
47
+ expect(subject).to have_excel_sheets(2)
48
+ end
49
+
50
+ it "has at least 3 sheets and a sheet named Sheet1" do
51
+ expect(subject).to have_excel_sheets([2, "Sheet1"])
52
+ end
53
+ ```
54
+
55
+ ### have_excel_columns
56
+
57
+ Tests that a specific sheet contains some columns with specific value on the first row
58
+
59
+ `subject` must be one of the following types:
60
+ - `String`: file path to an excel file (requires roo gem)
61
+ - `File`: file path to an excel file (requires roo gem)
62
+ - `Roo::Excelx`: excel file loaded with roo (requires roo gem)
63
+ - `Roo::Excelx::Sheet`: (requires roo gem)
64
+ - `Axlsx::Package`: (requires caxlsx gem)
65
+ - `Axlsx::Workbook`: (requires caxlsx gem)
66
+ - `Axlsx::Worksheet`: (requires caxlsx gem)
67
+
68
+ #### chaining
69
+
70
+ - `in_sheet(sheet_name)`: sheet where the data is expected. name (String) or index (Integer)
71
+ - **required** unless subject is a `Roo::Excelx::Sheet` or a `Axlsx::Worksheet`
72
+ - `exactly`: Match columns in order and fail if extra columns are found
73
+
74
+ #### examples
75
+ ```ruby
76
+ it "has a Gender and First Name columns in sheet Sheet 1" do
77
+ expect(subject).to have_excel_columns(["Gender", "First Name"]).in_sheet("Sheet1")
78
+ end
79
+
80
+ it "has column Last Name in first sheet" do
81
+ expect(subject).to have_excel_columns("Last Name").in_sheet(0)
82
+ end
83
+
84
+ it "has columns First Name and Last Name in first sheet, matching exactly" do
85
+ expect(subject).to have_excel_columns(["First Name", "Last Name"]).in_sheet(0).exactly
86
+ end
87
+ ```
88
+ ### have_excel_column
89
+
90
+ Tests that a specific sheet contains columns with specific value on the first row
91
+
92
+ `subject` must be one of the following types:
93
+ - `String`: file path to an excel file (requires roo gem)
94
+ - `File`: file path to an excel file (requires roo gem)
95
+ - `Roo::Excelx`: excel file loaded with roo (requires roo gem)
96
+ - `Roo::Excelx::Sheet`: (requires roo gem)
97
+ - `Axlsx::Package`: (requires caxlsx gem)
98
+ - `Axlsx::Workbook`: (requires caxlsx gem)
99
+ - `Axlsx::Worksheet`: (requires caxlsx gem)
100
+
101
+
102
+ #### chaining
103
+
104
+ - `in_sheet(sheet_name)`: sheet where the data is expected. name (String) or index (Integer)
105
+ - **required** unless subject is a `Roo::Excelx::Sheet` or a `Axlsx::Worksheet`
106
+
107
+ #### examples
108
+ ```ruby
109
+ it "has a Gender column in sheet Sheet 1" do
110
+ expect(subject).to have_excel_columns("Gender").in_sheet("Sheet1")
111
+ end
112
+
113
+ it "has a Last Name column in first sheet" do
114
+ expect(subject).to have_excel_columns("Last Name").in_sheet(0)
115
+ end
116
+ ```
117
+
118
+ ### have_excel_cells
119
+
120
+ Tests that a specific row in a specific sheet contains the expected cell values
121
+
122
+ `subject` must be one of the following types:
123
+ - `String`: file path to an excel file (requires roo gem)
124
+ - `File`: file path to an excel file (requires roo gem)
125
+ - `Roo::Excelx`: excel file loaded with roo (requires roo gem)
126
+ - `Roo::Excelx::Sheet`: (requires roo gem)
127
+ - `Axlsx::Package`: (requires caxlsx gem)
128
+ - `Axlsx::Workbook`: (requires caxlsx gem)
129
+ - `Axlsx::Worksheet`: (requires caxlsx gem)
130
+
131
+ #### chaining
132
+
133
+ - `in_sheet(sheet_name)`: sheet where the data is expected. name (String) or index (Integer, zero based)
134
+ - **required** unless subject is a `Roo::Excelx::Sheet` or a `Axlsx::Worksheet`
135
+ - `in_row(row_index)`: row where the data is expected (row_index: Integer, zero-based)
136
+
137
+ #### examples
138
+ ```ruby
139
+ it "has John Smith information on second row" do
140
+ expect(subject).to have_excel_cells(["John", "Smith", 24]).in_row(1).in_sheet("Sheet1")
141
+ end
142
+ ```
143
+
144
+ ### have_excel_empty_row
145
+
146
+ Tests that a specific row in a specific sheet is empty
147
+
148
+ `subject` must be one of the following types:
149
+ - `String`: file path to an excel file (requires roo gem)
150
+ - `File`: file path to an excel file (requires roo gem)
151
+ - `Roo::Excelx`: excel file loaded with roo (requires roo gem)
152
+ - `Roo::Excelx::Sheet`: (requires roo gem)
153
+ - `Axlsx::Package`: (requires caxlsx gem)
154
+ - `Axlsx::Workbook`: (requires caxlsx gem)
155
+ - `Axlsx::Worksheet`: (requires caxlsx gem)
156
+
157
+ #### chaining
158
+
159
+ - `in_sheet(sheet_name)`: sheet where the data is expected. name (String) or index (Integer)
160
+ - **required** unless subject is a `Roo::Excelx::Sheet` or a `Axlsx::Worksheet`
161
+
162
+
163
+ #### examples
164
+ ```ruby
165
+ it "has no data on third row" do
166
+ expect(subject).to have_excel_empty_row(2)
167
+ end
168
+ ```
169
+
170
+ ### have_excel_cell_value
171
+
172
+ Tests that a cell in a specific column of a specific row from a specific sheet has expected value
173
+
174
+ `subject` must be one of the following types:
175
+ - `String`: file path to an excel file (requires roo gem)
176
+ - `File`: file path to an excel file (requires roo gem)
177
+ - `Roo::Excelx`: excel file loaded with roo (requires roo gem)
178
+ - `Roo::Excelx::Sheet`: (requires roo gem)
179
+ - `Axlsx::Package`: (requires caxlsx gem)
180
+ - `Axlsx::Workbook`: (requires caxlsx gem)
181
+ - `Axlsx::Worksheet`: (requires caxlsx gem)
182
+
183
+ #### chaining
184
+
185
+ - `in_sheet(sheet_name)`: sheet where the data is expected. name (String) or index (Integer, zero based)
186
+ - **required** unless subject is a `Roo::Excelx::Sheet` or a `Axlsx::Worksheet`
187
+ - `in_row(row_index)`: row where the data is expected (row_index: Integer, zero-based)
188
+ - `column(first_row_value)`: value of the column in the first row
189
+
190
+
191
+ #### examples
192
+ ```ruby
193
+ it "has John in the First Name column of the third row of the first sheet" do
194
+ expect(subject).to have_excel_cell_value("John").in_sheet(0).in_row(2).in_column("First Name")
195
+ end
196
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # Base class for sheet based machers
6
+ class BaseSheet
7
+ include Utils
8
+ attr_reader :sheet_name, :sheet, :subject
9
+
10
+ def matches?(subject)
11
+ @subject = subject
12
+ @sheet = find_sheet
13
+ return false if sheet.nil?
14
+
15
+ process_sheet.tap do
16
+ finalize
17
+ end
18
+ end
19
+
20
+ def in_sheet(name)
21
+ @sheet_name = name
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ def find_sheet
28
+ find_axlsx_sheet || find_roo_sheet
29
+ rescue RangeError, ArgumentError
30
+ nil
31
+ end
32
+
33
+ def find_axlsx_sheet
34
+ return unless defined?(Axlsx)
35
+ return subject if subject.is_a?(Axlsx::Worksheet)
36
+
37
+ workbook = if subject.is_a?(Axlsx::Package)
38
+ subject.workbook
39
+ else
40
+ subject
41
+ end
42
+
43
+ return unless workbook.is_a?(Axlsx::Workbook)
44
+
45
+ axlsx_sheet_from_workbook(workbook)
46
+ end
47
+
48
+ def axlsx_sheet_from_workbook(workbook)
49
+ if sheet_name.is_a?(String)
50
+ workbook.sheet_by_name(sheet_name)
51
+ elsif sheet_name.is_a?(Integer)
52
+ workbook.worksheets[sheet_name]
53
+ else
54
+ raise ArgumentError, "Missing sheet name"
55
+ end
56
+ end
57
+
58
+ def find_roo_sheet
59
+ return unless defined?(Roo::Excelx)
60
+ return subject if subject.is_a?(Roo::Excelx::Sheet)
61
+
62
+ return unless roo_spreadsheet
63
+ return unless sheet_name
64
+
65
+ roo_spreadsheet.sheet_for(sheet_name)
66
+ end
67
+
68
+ def roo_spreadsheet
69
+ return @roo_spreadsheet unless @roo_spreadsheet.nil?
70
+
71
+ @roo_spreadsheet = if subject.is_a?(Roo::Excelx)
72
+ subject
73
+ elsif subject.is_a?(String) || subject.is_a?(File)
74
+ Roo::Spreadsheet.open(subject)
75
+ end
76
+ end
77
+
78
+ def process_sheet
79
+ if defined?(Axlsx) && sheet.is_a?(Axlsx::Worksheet)
80
+ process_axlsx_sheet
81
+ elsif defined?(Roo::Excelx) && sheet.is_a?(Roo::Excelx::Sheet)
82
+ process_roo_sheet
83
+ else
84
+ raise "Unsupported worksheet type: #{worksheet.class}"
85
+ end
86
+ end
87
+
88
+ def sheet_failure_message
89
+ if sheet_name
90
+ "Could not find sheet #{sheet_name}"
91
+ else
92
+ "Sheet not provided"
93
+ end
94
+ end
95
+
96
+ def failure_message_in_sheet(msg)
97
+ if sheet_name
98
+ "#{msg} in sheet #{sheet_name}"
99
+ else
100
+ msg
101
+ end
102
+ end
103
+
104
+ def finalize
105
+ roo_spreadsheet&.close
106
+ rescue StandardError => e
107
+ puts "Warning: error closing Roo Spreadsheet: #{e}"
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ ## have_excel_cell_value
6
+ class CellValue < BaseSheet
7
+ include InRow
8
+ include InColumn
9
+ attr_reader :expected_value, :actual_value
10
+
11
+ def initialize(expected_value)
12
+ super()
13
+ @expected_value = expected_value
14
+ end
15
+
16
+ def failure_message
17
+ return sheet_failure_message if sheet.nil?
18
+ return header_column_not_found_message if header_column_not_found?
19
+ return row_not_found_message if row_not_found?
20
+ return column_not_found_message if column_not_found?
21
+
22
+ "Mismatch cell value in column '#{column_name}' of row '#{row_index}': " \
23
+ "expected: '#{expected_value}', received: '#{actual_value}'"
24
+ end
25
+
26
+ private
27
+
28
+ def process_axlsx_sheet
29
+ return false if header_column_not_found?
30
+ return false if row.nil?
31
+ return false if column_index.nil?
32
+
33
+ @actual_value = row[column_index]&.value
34
+ perform_match
35
+ end
36
+
37
+ def process_roo_sheet
38
+ return false if header_column_not_found?
39
+ return false if row.nil?
40
+ return false if column_index.nil?
41
+
42
+ @actual_value = row[column_index]
43
+ perform_match
44
+ end
45
+
46
+ def perform_match
47
+ convert_to_excel_value(expected_value) == actual_value
48
+ end
49
+
50
+ def convert_to_excel_value(value)
51
+ case value
52
+ when TrueClass
53
+ "1"
54
+ when FalseClass
55
+ "0"
56
+ else
57
+ value
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # have_excel_cells
6
+ class Cells < BaseSheet
7
+ include InRow
8
+ attr_reader :expected_cells, :actual_cells, :mismatch_indexes
9
+
10
+ EXPECTED = "Expected"
11
+ RECEIVED = "Received"
12
+
13
+ def initialize(expected_cells)
14
+ super()
15
+ @expected_cells = force_array(expected_cells)
16
+ @actual_cells = []
17
+ @mismatch_indexes = []
18
+ end
19
+
20
+ def failure_message # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
21
+ return sheet_failure_message if sheet.nil?
22
+ return row_not_found_message if row.nil?
23
+
24
+ message = failure_message_header(String.new("Rows did not match : \n"))
25
+ # Cells content
26
+ expected_cells.each_with_index do |expected_cell, idx|
27
+ sizeof_actual_cell = actual_cells[idx].to_s.size
28
+ sizeof_expected_cell = expected_cell.to_s.size
29
+ message << " #{idx}"
30
+ message << (" " * (4 - idx.to_s.size))
31
+ message << "| #{actual_cells[idx]}"
32
+ message << (" " * (biggest_actual_cell_size - sizeof_actual_cell))
33
+ message << " | #{expected_cell}"
34
+ message << (" " * (biggest_expected_cell_size - sizeof_expected_cell))
35
+ message << " | #{mismatch_indexes.include?(idx) ? "<----- Mismatch" : ""}"
36
+ message << "\n"
37
+ end
38
+ message
39
+ end
40
+
41
+ private
42
+
43
+ def process_axlsx_sheet
44
+ return false if row.nil?
45
+
46
+ @actual_cells = row.cells.map(&:value)
47
+ perform_match
48
+ end
49
+
50
+ def process_roo_sheet
51
+ return false if row.nil?
52
+
53
+ @actual_cells = row
54
+ perform_match
55
+ end
56
+
57
+ def perform_match
58
+ expected_cells.each_with_index do |expected_cell, idx|
59
+ actual_cell = actual_cells[idx]
60
+ mismatch_indexes << idx if actual_cell.to_s != expected_cell.to_s
61
+ end
62
+ mismatch_indexes.empty?
63
+ end
64
+
65
+ def biggest_actual_cell_size
66
+ @biggest_actual_cell_size ||= (actual_cells.map { |c| c.to_s.size } + [RECEIVED.size]).max
67
+ end
68
+
69
+ def biggest_expected_cell_size
70
+ @biggest_expected_cell_size ||= (expected_cells.map { |c| c.to_s.size } + [EXPECTED.size]).max
71
+ end
72
+
73
+ def failure_message_header(message) # rubocop:disable Metrics/AbcSize
74
+ # Table Header
75
+ message << " | #{RECEIVED}"
76
+ message << (" " * (biggest_actual_cell_size - RECEIVED.size))
77
+ message << " | #{EXPECTED}"
78
+ message << (" " * (biggest_expected_cell_size - EXPECTED.size))
79
+ message << " |\n"
80
+ # Header / Content separator
81
+ message << ("#{"-" * 5}|#{"-" * (2 + biggest_actual_cell_size)}|#{"-" * (2 + @biggest_expected_cell_size)}|\n")
82
+ message
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # have_excel_columns / have_excel_column
6
+ class Columns < BaseSheet
7
+ include ExactMatch
8
+ attr_reader :extra_columns, :missing_columns, :expected_columns, :actual_columns
9
+
10
+ def initialize(expected_columns)
11
+ super()
12
+ @expected_columns = force_array(expected_columns)
13
+ @missing_columns = []
14
+ @extra_columns = []
15
+ @actual_columns = []
16
+ end
17
+
18
+ def failure_message
19
+ return sheet_failure_message if sheet.nil?
20
+
21
+ msg = failure_message_in_sheet("Columns mismatch")
22
+
23
+ "#{msg}:\n\t#{column_failure_message}"
24
+ end
25
+
26
+ private
27
+
28
+ def process_axlsx_sheet
29
+ @actual_columns = sheet.rows[0]&.map(&:value)
30
+ if exact_match
31
+ perform_exact_match
32
+ else
33
+ perform_loose_match
34
+ end
35
+ end
36
+
37
+ def process_roo_sheet
38
+ @actual_columns = sheet.row(1)
39
+ if exact_match
40
+ perform_exact_match
41
+ else
42
+ perform_loose_match
43
+ end
44
+ end
45
+
46
+ def perform_exact_match
47
+ return true if expected_columns == actual_columns
48
+
49
+ (expected_columns - actual_columns).each do |column|
50
+ extra_columns << column
51
+ end
52
+
53
+ (actual_columns - expected_columns).each do |column|
54
+ missing_columns << column
55
+ end
56
+ false
57
+ end
58
+
59
+ def perform_loose_match
60
+ expected_columns.each do |column|
61
+ missing_columns << column unless actual_columns.include?(column)
62
+ end
63
+ missing_columns.empty?
64
+ end
65
+
66
+ def column_failure_message
67
+ column_mismatch_failure_message || column_order_failure_message
68
+ end
69
+
70
+ def column_order_failure_message
71
+ return unless exact_match
72
+
73
+ "Expected: #{map_output(expected_columns)}\n\t" \
74
+ "Received: #{map_output(actual_columns)}"
75
+ end
76
+
77
+ def column_mismatch_failure_message
78
+ msg = [missing_column_failure_message, extra_columns_failure_message].compact.join("\n\t")
79
+ if msg.empty?
80
+ nil
81
+ else
82
+ msg
83
+ end
84
+ end
85
+
86
+ def missing_column_failure_message
87
+ return if missing_columns.empty?
88
+
89
+ "Missing columns: #{map_output(missing_columns)}"
90
+ end
91
+
92
+ def extra_columns_failure_message
93
+ return if extra_columns.empty?
94
+
95
+ "Extra columns: #{map_output(extra_columns)}"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # have_excel_empty_row
6
+ class EmptyRow < BaseSheet
7
+ attr_reader :row_index
8
+
9
+ def initialize(row_index)
10
+ super()
11
+ @row_index = row_index
12
+ end
13
+
14
+ def failure_message_when_negated
15
+ return sheet_failure_message if sheet.nil?
16
+
17
+ "Row at index '#{row_index}' was expected to NOT be empty, but was empty."
18
+ end
19
+
20
+ def failure_message
21
+ return sheet_failure_message if sheet.nil?
22
+
23
+ "Row at index '#{row_index}' was expected to be empty, but was not empty."
24
+ end
25
+
26
+ private
27
+
28
+ def process_axlsx_sheet
29
+ row = sheet.rows[row_index]
30
+ true if row.nil?
31
+ end
32
+
33
+ def process_roo_sheet
34
+ row = sheet.row(row_index).compact
35
+ true if row.empty?
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # common base for matchers with exact match option
6
+ module ExactMatch
7
+ attr_reader :exact_match
8
+
9
+ def exactly
10
+ @exact_match = true
11
+ self
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # common base for columns matchers
6
+ module InColumn
7
+ attr_reader :column_name
8
+
9
+ def in_column(column_name)
10
+ @column_name = column_name
11
+ self
12
+ end
13
+
14
+ private
15
+
16
+ def column_index
17
+ return @column_index unless @column_index.nil?
18
+
19
+ return if sheet.nil? || column_row.nil?
20
+
21
+ find_column
22
+ @column_index
23
+ end
24
+
25
+ def column_row
26
+ return @column_row unless @column_row.nil?
27
+
28
+ @column_row = axlsx_column_row
29
+ @column_row = roo_column_row if @column_row.nil?
30
+ @column_row
31
+ end
32
+
33
+ def axlsx_column_row
34
+ sheet.rows[0]&.map(&:value) if defined?(Axlsx::Worksheet) && sheet.is_a?(Axlsx::Worksheet)
35
+ end
36
+
37
+ def roo_column_row
38
+ sheet.row(1) if defined?(Roo::Excelx) && sheet.is_a?(Roo::Excelx::Sheet)
39
+ rescue StandardError
40
+ # do nothing
41
+ end
42
+
43
+ def find_column
44
+ column_row.each_with_index do |c, i|
45
+ break unless @column_index.nil?
46
+
47
+ @column_index = i if c.to_s == column_name
48
+ end
49
+ end
50
+
51
+ def header_column_not_found?
52
+ column_row.nil? || column_row.compact.empty?
53
+ end
54
+
55
+ def header_column_not_found_message
56
+ "First row not found" if header_column_not_found?
57
+ end
58
+
59
+ def column_not_found?
60
+ column_index.nil?
61
+ end
62
+
63
+ def column_not_found_message
64
+ "Column #{column_name} not found"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # common base row based matchers
6
+ module InRow
7
+ attr_reader :row_index
8
+
9
+ def in_row(row_index)
10
+ @row_index = row_index
11
+ self
12
+ end
13
+
14
+ private
15
+
16
+ def row
17
+ return if sheet.nil?
18
+ return if row_index.nil?
19
+
20
+ @row ||= find_row
21
+ end
22
+
23
+ def find_row
24
+ if defined?(Axlsx::Worksheet) && sheet.is_a?(Axlsx::Worksheet)
25
+ sheet.rows[row_index]
26
+ elsif defined?(Roo::Excelx) && sheet.is_a?(Roo::Excelx::Sheet)
27
+ sheet.row(row_index + 1)
28
+ end
29
+ end
30
+
31
+ def row_not_found_message
32
+ "Row #{row_index} is empty" if row_not_found?
33
+ end
34
+
35
+ def row_not_found?
36
+ row.nil? || row.compact.empty?
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # have_excel_sheets
6
+ class Sheets
7
+ include Utils
8
+ attr_reader :errors, :expected_sheet_names
9
+
10
+ def initialize(expected_sheet_names)
11
+ @expected_sheet_names = force_array expected_sheet_names
12
+ @errors = []
13
+ end
14
+
15
+ def matches?(subject)
16
+ if subject.is_a?(String) || subject.is_a?(File)
17
+ match_string(subject)
18
+ elsif defined?(Roo::Excelx) && subject.is_a?(Roo::Excelx)
19
+ match_roo_excelx(subject)
20
+ elsif defined?(Axlsx)
21
+ matches_axlsx?(subject)
22
+ else
23
+ invalid_file
24
+ end
25
+ end
26
+
27
+ def failure_message
28
+ "Xlsx file sheets not found: #{errors.map { |s| "'#{s}'" }.join(",")}"
29
+ end
30
+
31
+ private
32
+
33
+ def matches_axlsx?(subject)
34
+ if subject.is_a?(Axlsx::Package)
35
+ match_caxlsx(subject.workbook)
36
+ elsif defined?(Axlsx) && subject.is_a?(Axlsx::Workbook)
37
+ match_caxlsx(subject)
38
+ else
39
+ invalid_file
40
+ end
41
+ end
42
+
43
+ def invalid_file
44
+ raise ArgumentError, "Could not evaluate the sheets existance, " \
45
+ "the matcher expected an instance of Roo::Excelx, " \
46
+ "Axlsx::Package or Axlsx::Workbook, got #{excel_file.class}."
47
+ end
48
+
49
+ def match_caxlsx(excel_file)
50
+ return invalid_file unless defined?(Axlsx)
51
+
52
+ workbook = if excel_file.is_a?(Axlsx::Package)
53
+ excel_file.workbook
54
+ elsif excel_file.is_a?(Axlsx::Workbook)
55
+ excel_file
56
+ end
57
+
58
+ return invalid_file if workbook.nil?
59
+
60
+ match_caxlsx_workbook(workbook)
61
+ end
62
+
63
+ def match_caxlsx_workbook(workbook)
64
+ expected_sheet_names.each do |expected_sheet_name|
65
+ found = if expected_sheet_name.is_a?(String)
66
+ workbook.sheet_by_name(expected_sheet_name)
67
+ elsif expected_sheet_name.is_a?(Integer)
68
+ workbook.worksheets[expected_sheet_name]
69
+ end
70
+
71
+ errors << expected_sheet_name unless found.is_a?(Axlsx::Worksheet)
72
+ end
73
+ errors.empty?
74
+ end
75
+
76
+ def match_string(excel_file)
77
+ if defined?(Roo::Spreadsheet)
78
+ roo_file = Roo::Spreadsheet.open(excel_file)
79
+ match_roo_excelx(roo_file)
80
+ else
81
+ raise ArgumentError, "Could not evaluate the sheets existance, " \
82
+ "the matcher received a string, but Roo::Spreadsheet is not defined"
83
+ end
84
+ end
85
+
86
+ def match_roo_excelx(excel_file)
87
+ expected_sheet_names.each do |expected_sheet_name|
88
+ excel_file.sheet(expected_sheet_name)
89
+ rescue ArgumentError, RangeError
90
+ errors << expected_sheet_name
91
+ rescue TypeError => e
92
+ errors << "#{expected_sheet_name} : #{e.message}"
93
+ end
94
+ errors.empty?
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ # internal utilities
6
+ module Utils
7
+ def force_array(value)
8
+ if value.is_a?(Array)
9
+ value
10
+ else
11
+ [value]
12
+ end
13
+ end
14
+
15
+ def map_output(array)
16
+ array.map do |v|
17
+ if v.is_a?(String)
18
+ "'#{v}'"
19
+ else
20
+ v
21
+ end
22
+ end.join(", ")
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpec
4
+ module XlsxMatchers
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "xlsx_matchers/version"
4
+ require_relative "xlsx_matchers/exact_match"
5
+ require_relative "xlsx_matchers/in_row"
6
+ require_relative "xlsx_matchers/in_column"
7
+ require_relative "xlsx_matchers/utils"
8
+ require_relative "xlsx_matchers/base_sheet"
9
+
10
+ require_relative "xlsx_matchers/sheets"
11
+ require_relative "xlsx_matchers/columns"
12
+ require_relative "xlsx_matchers/empty_row"
13
+ require_relative "xlsx_matchers/cells"
14
+ require_relative "xlsx_matchers/cell_value"
15
+
16
+ begin
17
+ require "roo"
18
+ rescue LoadError
19
+ # optional dependency
20
+ end
21
+
22
+ begin
23
+ require "caxlsx"
24
+ rescue LoadError
25
+ # optional dependency
26
+ end
27
+ module RSpec
28
+ # # RSpec::XlsxMatchers adds the following matchers to rspec
29
+ # - have_excel_sheets
30
+ # - have_excel_columns
31
+ # - have_excel_column
32
+ # - have_excel_empty_row
33
+ # - have_excel_cells
34
+ # - have_excel_cell_value
35
+ module XlsxMatchers
36
+ # class Error < StandardError; end
37
+
38
+ def have_excel_sheets(sheet_names)
39
+ Sheets.new(sheet_names)
40
+ end
41
+
42
+ def have_excel_columns(column_names)
43
+ Columns.new(column_names)
44
+ end
45
+
46
+ def have_excel_column(column_name)
47
+ raise ArgumentError, "Column name should not be an Array" if column_name.is_a?(Array)
48
+
49
+ Columns.new([column_name])
50
+ end
51
+
52
+ def have_excel_empty_row(index)
53
+ EmptyRow.new(index)
54
+ end
55
+
56
+ def have_excel_cells(cells)
57
+ Cells.new(cells)
58
+ end
59
+
60
+ def have_excel_cell_value(value)
61
+ CellValue.new(value)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ module RSpec
2
+ module XlsxMatchers
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-xlsx_matchers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - drvn-eb
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-06-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ description: RSpec Xlsx matchers
28
+ email:
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".rspec"
34
+ - ".rubocop.yml"
35
+ - CHANGELOG.md
36
+ - LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - lib/rspec/xlsx_matchers.rb
40
+ - lib/rspec/xlsx_matchers/base_sheet.rb
41
+ - lib/rspec/xlsx_matchers/cell_value.rb
42
+ - lib/rspec/xlsx_matchers/cells.rb
43
+ - lib/rspec/xlsx_matchers/columns.rb
44
+ - lib/rspec/xlsx_matchers/empty_row.rb
45
+ - lib/rspec/xlsx_matchers/exact_match.rb
46
+ - lib/rspec/xlsx_matchers/in_column.rb
47
+ - lib/rspec/xlsx_matchers/in_row.rb
48
+ - lib/rspec/xlsx_matchers/sheets.rb
49
+ - lib/rspec/xlsx_matchers/utils.rb
50
+ - lib/rspec/xlsx_matchers/version.rb
51
+ - sig/rspec/xlsx_matchers.rbs
52
+ homepage: https://github.com/dreeven-oss/rspec-xlsx_matchers
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/dreeven-oss/rspec-xlsx_matchers
57
+ source_code_uri: https://github.com/dreeven-oss/rspec-xlsx_matchers
58
+ rubygems_mfa_required: 'true'
59
+ post_install_message:
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.0.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.4.19
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: RSpec Xlsx matchers
78
+ test_files: []