# frozen_string_literal: true Capybara.add_selector(:table, locator_type: [String, Symbol]) do xpath do |locator, caption: nil, **| xpath = XPath.descendant(:table) unless locator.nil? locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s) locator_matchers |= XPath.attr(test_id) == locator if test_id xpath = xpath[locator_matchers] end xpath = xpath[XPath.descendant(:caption) == caption] if caption xpath end expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols| col_conditions = cols.map do |col| if col.is_a? Hash col.reduce(nil) do |xp, (header, cell_str)| header = XPath.descendant(:th)[XPath.string.n.is(header)] td = XPath.descendant(:tr)[header].descendant(:td) cell_condition = XPath.string.n.is(cell_str) if xp prev_cell = XPath.ancestor(:table)[1].join(xp) cell_condition &= (prev_cell & prev_col_position?(prev_cell)) end td[cell_condition] end else cells_xp = col.reduce(nil) do |prev_cell, cell_str| cell_condition = XPath.string.n.is(cell_str) if prev_cell prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell) cell_condition &= (prev_cell & prev_col_position?(prev_cell)) end XPath.descendant(:td)[cell_condition] end XPath.descendant(:tr).join(cells_xp) end end.reduce(:&) xpath[col_conditions] end expression_filter(:cols, valid_values: [Array]) do |xpath, cols| raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all?(Array) rows = cols.transpose col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&) xpath[match_row_count(rows.size)][col_conditions] end expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows| rows_conditions = rows.map { |row| match_row(row) }.reduce(:&) xpath[rows_conditions] end expression_filter(:rows, valid_values: [Array]) do |xpath, rows| rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&) xpath[match_row_count(rows.size)][rows_conditions] end describe_expression_filters do |caption: nil, **| " with caption \"#{caption}\"" if caption end def prev_col_position?(cell) XPath.position.equals(cell_position(cell)) end def cell_position(cell) cell.preceding_sibling(:td).count.plus(1) end def match_row(row, match_size: false) xp = XPath.descendant(:tr)[ if row.is_a? Hash row_match_cells_to_headers(row) else XPath.descendant(:td)[row_match_ordered_cells(row)] end ] xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size xp end def match_row_count(size) XPath.descendant(:tbody).descendant(:tr).count.equals(size) | (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody)) end 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.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)] end row_conditions.reverse.reduce do |cond, cell| cell[XPath.following_sibling[cond]] end end end