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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE +21 -0
- data/README.md +196 -0
- data/Rakefile +12 -0
- data/lib/rspec/xlsx_matchers/base_sheet.rb +111 -0
- data/lib/rspec/xlsx_matchers/cell_value.rb +62 -0
- data/lib/rspec/xlsx_matchers/cells.rb +86 -0
- data/lib/rspec/xlsx_matchers/columns.rb +99 -0
- data/lib/rspec/xlsx_matchers/empty_row.rb +39 -0
- data/lib/rspec/xlsx_matchers/exact_match.rb +15 -0
- data/lib/rspec/xlsx_matchers/in_column.rb +68 -0
- data/lib/rspec/xlsx_matchers/in_row.rb +40 -0
- data/lib/rspec/xlsx_matchers/sheets.rb +98 -0
- data/lib/rspec/xlsx_matchers/utils.rb +26 -0
- data/lib/rspec/xlsx_matchers/version.rb +7 -0
- data/lib/rspec/xlsx_matchers.rb +64 -0
- data/sig/rspec/xlsx_matchers.rbs +6 -0
- metadata +78 -0
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
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
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,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,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,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
|
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: []
|