rspec-xlsx_matchers 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.
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: []