Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting tables that have both horizontal and vertical headers #2703

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/capybara/node/matchers.rb
Expand Up @@ -517,13 +517,13 @@ def has_no_select?(locator = nil, **options, &optional_filter_block)
#
# @param [String] locator The id or caption of a table
# @option options [Array<Array<String>>] :rows
# Text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
# Text which should be contained in the tables `<td>` and `<th>` elements organized by row (`<td>` and `<th>` visibility is not considered)
# @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_rows
# Partial set of text which should be contained in the tables `<td>` elements organized by row (`<td>` visibility is not considered)
# Partial set of text which should be contained in the tables `<td>` and `<th>` elements organized by row (`<td>` and `<th>` visibility is not considered)
# @option options [Array<Array<String>>] :cols
# Text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
# Text which should be contained in the tables `<td>` and `<th>` elements organized by row (`<td>` and `<th>` visibility is not considered)
# @option options [Array<Array<String>>, Array<Hash<String,String>>] :with_cols
# Partial set of text which should be contained in the tables `<td>` elements organized by column (`<td>` visibility is not considered)
# Partial set of which should be contained in the tables `<td>` and `<th>` elements organized by row (`<td>` and `<th>` visibility is not considered)
# @return [Boolean] Whether it exists
#
def has_table?(locator = nil, **options, &optional_filter_block)
Expand Down
27 changes: 22 additions & 5 deletions lib/capybara/selector/definition/table.rb
Expand Up @@ -52,6 +52,7 @@

expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)

xpath[rows_conditions]
end

Expand All @@ -77,10 +78,25 @@ def match_row(row, match_size: false)
if row.is_a? Hash
row_match_cells_to_headers(row)
else
XPath.descendant(:td)[row_match_ordered_cells(row)]
XPath.descendant(:td, :th)[row_match_ordered_cells(row)]
end
]
xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size

xp = xp[
# there's no THEAD for this TR
XPath.ancestor(:table)[XPath.child(:thead)].not
# expect as many TDs (and only TDs) as provided by the user
.and(XPath.descendant(:td).count.equals(row.size))
# bacause nodes other than TDs act as headers
.or(
# there's a THEAD for this TR
XPath.ancestor(:table)[XPath.child(:thead)]
# expect as many TDs or THs as provided by the user
.and(XPath.descendant(:td, :th).count.equals(row.size))
# because they are all considered row cells in that case
)
] if match_size

xp
end

Expand All @@ -92,16 +108,17 @@ def match_row_count(size)
def row_match_cells_to_headers(row)
row.map do |header, cell|
header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
XPath.descendant(:td)[
XPath.descendant(:td, :th)[
XPath.string.n.is(cell) & header_xp.boolean & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
]
end.reduce(:&)
end

def row_match_ordered_cells(row)
row_conditions = row.map do |cell|
XPath.self(:td)[XPath.string.n.is(cell)]
row_conditions = row.map do |cell|
XPath.self(:td, :th)[XPath.string.n.is(cell)]
end

row_conditions.reverse.reduce do |cond, cell|
cell[XPath.following_sibling[cond]]
end
Expand Down
37 changes: 37 additions & 0 deletions lib/capybara/spec/session/has_table_spec.rb
Expand Up @@ -59,6 +59,34 @@
])
end

context 'with a table that has both horizontal and vertical headers' do
it 'should accept arrays representing rows' do
expect(@session).to have_table('Horizontal and Vertical Headers', rows:
[
%w[Tim London 555-1234],
%w[Joe Berlin 555-5678],
])
end

it 'should key rows by vertical headers' do
expect(@session).to have_table('Horizontal and Vertical Headers', with_rows:
[
{ 'Name' => 'Tim', 'City' => 'London', 'Phone' => '555-1234' },
{ 'Name' => 'Joe', 'City' => 'Berlin', 'Phone' => '555-5678' },
])
end

it 'should match all cols with array of cell values' do
expect(@session).to have_table('Horizontal and Vertical Headers', cols:
[
%w[Tim Joe],
%w[London Berlin],
%w[555-1234 555-5678],
]
)
end
end

it 'should match with vertical headers' do
expect(@session).to have_table('Vertical Headers', with_cols:
[
Expand Down Expand Up @@ -115,6 +143,15 @@
])
end

it "should accept arrays representing TDs of rows with THs" do
expect(@session).to have_table('Vertical Headers', with_rows:
[
["Thomas", "Danilo", "Vern", "Ratke", "Palmer"],
["Walpole", "Wilkinson", "Konopelski", "Lawrence", "Sawayn"],
["Oceanside", "Johnsonville", "Everette", "East Sorayashire", "West Trinidad"]
])
end

it 'should be false if the table is not on the page' do
expect(@session).not_to have_table('Monkey')
end
Expand Down
23 changes: 23 additions & 0 deletions lib/capybara/spec/views/tables.erb
Expand Up @@ -62,6 +62,29 @@
</table>
</form>

<table>
<caption>Horizontal and Vertical Headers</caption>
<thead>
<tr>
<th>Name</th>
<th>City</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
<tr>
<th>Tim</th>
<td>London</td>
<td>555-1234</td>
</tr>
<tr>
<th>Joe</th>
<td>Berlin</td>
<td>555-5678</td>
</tr>
</tbody>
</table>

<table>
<caption>Horizontal Headers</caption>
<thead>
Expand Down