/*******************************************************************************
* Copyright 2014 JHC Systems Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package jhc.redsniff.webdriver.table.matchers;
import static com.google.common.base.Joiner.on;
import static com.google.common.collect.Lists.newArrayList;
import static jhc.redsniff.webdriver.table.matchers.IsTableCellInColumn.isInColumnThat;
import static jhc.redsniff.webdriver.table.matchers.IsTableColumn.isColumnIdentifiedBy;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jhc.redsniff.html.tables.TableCell;
import jhc.redsniff.html.tables.TableColumn;
import jhc.redsniff.html.tables.TableRow;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
/**
* A matcher of {@link TableRow}s that matches if the row contains the supplied
* cell in the given column
*
* @see {@link IsTableRowConsistingOf}
* @author InfanteN
*
*/
public class IsTableRowIncluding extends TableRowMatcher {
List<Matcher<TableCell>> cellMatchers;
private String matcherTextToUseForDescription;
protected IsTableRowIncluding(List<Matcher<TableCell>> cellMatchers) {
this(cellMatchers, "includes cells");
}
private IsTableRowIncluding(List<Matcher<TableCell>> cellMatchers, String matcherTextToUseForDescription) {
this.cellMatchers = cellMatchers;
this.matcherTextToUseForDescription = matcherTextToUseForDescription;
}
public void describeTo(Description description) {
description.appendList(matcherTextToUseForDescription + ": ", " and ", "", cellMatchers);
}
@Override
protected boolean matchesSafely(TableRow actualRow, Description mismatchDescription) {
Matching matching = doMatching(actualRow, mismatchDescription);
if (matching.isFinished())
return true;
else {
matching.describeMismatchesTo(mismatchDescription);
matching.describeWholeRowHighlightingMismatchCellsTo(mismatchDescription);
return false;
}
}
protected Matching doMatching(TableRow actualRow,
Description mismatchDescription) {
Matching matching = new Matching(cellMatchers, actualRow, mismatchDescription);
for (TableCell actualCell : actualRow.cells())
matching.findMatchFor(actualCell);
return matching;
}
protected class Matching {
private final List<Matcher<TableCell>> remainingMatchers;
private final TableRow actualRow;
private Map<CellInColumnMatcher, TableCell> mismatchedColumnMapping = new LinkedHashMap<CellInColumnMatcher, TableCell>();
public Matching(List<Matcher<TableCell>> matchers, TableRow actualRow, Description mismatchDescription) {
this.remainingMatchers = newArrayList(matchers);
this.actualRow = actualRow;
}
public void describeWholeRowHighlightingMismatchCellsTo(Description mismatchDescription) {
TableRowMatcher.describeWholeRowHighlightingMismatchCells(actualRow, mismatchDescription,
columnsOf(mismatchedColumnMapping.values()));
}
public void describeMismatchesTo(Description mismatchDescription) {
for (CellInColumnMatcher cellInColumnMatcher : mismatchedColumnMapping.keySet()) {
TableCell actualCell = mismatchedColumnMapping.get(cellInColumnMatcher);
cellInColumnMatcher.describeMismatchIfCellIsForColumn(actualCell, mismatchDescription);
}
if (!unmatchedMatchersForUnknownColumn().isEmpty())
describeUnmatchedColumnsTo(mismatchDescription);
}
private void describeUnmatchedColumnsTo(Description mismatchDescription) {
mismatchDescription.appendText("\nNot matched:");
for (Matcher<TableCell> unmatchedCellMatcher : unmatchedMatchersForUnknownColumn()) {
mismatchDescription.appendDescriptionOf(unmatchedCellMatcher);
if (unmatchedCellMatcher instanceof CellInColumnMatcher) {
mismatchDescription.appendText(" - No column matched: ");
((CellInColumnMatcher) unmatchedCellMatcher).columnMatcher.describeTo(mismatchDescription);
}
}
mismatchDescription.appendText("\nColumn headings were:\n");
mismatchDescription.appendText(on(", ").join(columnsOf(actualRow.cells())));
}
public List<Matcher<TableCell>> unmatchedMatchersForUnknownColumn() {
return remainingMatchers;
}
public void findMatchFor(TableCell actualCell) {
for (Matcher<TableCell> matcher : remainingMatchers) {
if (matcher.matches(actualCell)) {
// TODO could store mismatch descriptions here
// we've satisfied the matcher - remove it from list of
// matchers
remainingMatchers.remove(matcher);
return;
}
else if (canMatchToAColumn(actualCell, matcher)) {
mismatchedColumnMapping.put((CellInColumnMatcher) matcher, actualCell);
remainingMatchers.remove(matcher);
return;
}
}
// we've gone through all matchers and not matched this cell. That's
// ok - we only care about satisfying all our matchers but keep note
// of it
// for figuring out failure messages - ie figuring out which of the
// unmatched cells goes with which unmatched matcher.
}
private boolean canMatchToAColumn(TableCell actualCell, Matcher<TableCell> matcher) {
if (matcher instanceof CellInColumnMatcher) {
return ((CellInColumnMatcher) matcher).matchesColumnButNotContents(actualCell);
}
return false;
}
public boolean isFinished() {
return remainingMatchers.isEmpty() && mismatchedColumnMapping.isEmpty();
}
}
private static class CellInColumnMatcher extends TypeSafeMatcher<TableCell> {
private Matcher<TableColumn> columnMatcher;
private Matcher<TableCell> cellContentsMatcher;
public CellInColumnMatcher(Matcher<TableColumn> columnMatcher, Matcher<TableCell> cellContentsMatcher) {
this.columnMatcher = columnMatcher;
this.cellContentsMatcher = cellContentsMatcher;
}
public void describeTo(Description description) {
description.appendDescriptionOf(cellContentsMatcher).appendText(" in column ")
.appendDescriptionOf(columnMatcher);
}
public boolean matchesColumn(TableCell actualCell) {
return isInColumnThat(columnMatcher).matches(actualCell);
}
public boolean matchesContents(TableCell actualCell) {
return cellContentsMatcher.matches(actualCell);
}
public boolean matchesColumnButNotContents(TableCell actualCell) {
return matchesColumn(actualCell) && !matchesContents(actualCell);
}
@Override
protected boolean matchesSafely(TableCell actualCell) {
return matchesColumn(actualCell) && matchesContents(actualCell);
}
public void describeMismatchIfCellIsForColumn(TableCell actualCell, Description mismatchDescription) {
if (matchesColumnButNotContents(actualCell)) {
mismatchDescription
.appendText("cell in column: ")
.appendText(actualCell.columnDescription())
.appendText(", expected to be ")
.appendDescriptionOf(cellContentsMatcher)
.appendText("\n");
cellContentsMatcher.describeMismatch(actualCell, mismatchDescription);
//TODO could fetch back mismatch descriptions here
}
}
}
@Factory
public static Matcher<TableCell> valueInColumn(Object columnIdentifier, String cellContents) {
return new CellInColumnMatcher(IsTableColumn.isColumnIdentifiedBy(columnIdentifier), IsTableCellWithText
.cellWithText(cellContents));
}
@Factory
public static Matcher<TableCell> valueInColumn(Object columnIdentifier, Matcher<TableCell> cellMatcher) {
return new CellInColumnMatcher(isColumnIdentifiedBy(columnIdentifier), cellMatcher);
}
@SafeVarargs
@Factory
public static Matcher<TableRow> includesCells(Matcher<TableCell>... included) {
return new IsTableRowIncluding(Arrays.asList(included));
}
@SafeVarargs
@Factory
public static Matcher<TableRow> rowIncluding(Matcher<TableCell>... included) {
return new IsTableRowIncluding(Arrays.asList(included), "Row including");
}
@Override
public IndexCellMatcher indexedBy() {
return new IndexCellMatcher(cellMatchers.subList(0, 1));
// default to index on the first column mentioned in the expectation
}
}