/*******************************************************************************
* 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.collect.Lists.newArrayList;
import static java.util.Arrays.asList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jhc.redsniff.html.tables.Table;
import jhc.redsniff.html.tables.TableRow;
import jhc.redsniff.internal.matchers.CheckAndDiagnoseTogetherMatcher;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
/**
* A matcher of {@link Table}s that matches if the table's data rows contain rows matching the supplied matchers
* @see {@link IsTableRowIncluding} , {@link IsTable}
* @author InfanteN
*
*/
public class IsTableIncludingRows extends CheckAndDiagnoseTogetherMatcher<Table>{
List<Matcher<TableRow>> rowMatchers;
public IsTableIncludingRows(List<Matcher<TableRow>> rowMatchers) {
this.rowMatchers = rowMatchers;
}
public void describeTo(Description description) {
description.appendList("containing data rows:\n", " and \n ", "\n", rowMatchers);
}
@Override
protected boolean matchesSafely(Table actualTable, Description mismatchDescription) {
Matching matching = new Matching(rowMatchers,actualTable);
for(TableRow actualRow:actualTable.dataRows())
matching.findMatchFor(actualRow);
if(matching.isFinished())
return true;
else{
mismatchDescription.appendText(" had:");
matching.describeMismatches(mismatchDescription);
return false;
}
}
private class Matching {
private final List<Matcher<TableRow>> remainingMatchers;
private Map<TableRowMatcher,TableRow> mismatchedRowMapping = new LinkedHashMap<TableRowMatcher,TableRow>();
private Table actualTable;
public Matching(List<Matcher<TableRow>> matchers,Table actualTable) {
this.remainingMatchers = newArrayList(matchers);
this.actualTable = actualTable;
}
private void describeWholeTableCentredOnFirstMismatchRow(Table actualTable, Description mismatchDescription) {
mismatchDescription.appendText("\n" + actualTable.toStringCenteredOnRow(3,firstMismatchRowIndex()));
}
private int firstMismatchRowIndex(){
return mismatchedRowMapping.values().iterator().next().index(); //might not actually be the first row in table
}
public void describeMismatches(Description mismatchDescription) {
for(TableRowMatcher rowInTableMatcher:mismatchedRowMapping.keySet()){
TableRow actualRow = mismatchedRowMapping.get(rowInTableMatcher);
describeMismatchIfMatchesIndexCell(rowInTableMatcher,actualRow,mismatchDescription);
}
if(!unmatchedMatchers().isEmpty()){
mismatchDescription.appendText("\nCould not find any rows matching:\n ");
for(Matcher<TableRow> unmatchedRowMatcher:unmatchedMatchers())
mismatchDescription.appendText(">").appendDescriptionOf(unmatchedRowMatcher).appendText("\n");
}
describeWholeTableShowingMismatches(mismatchDescription);
}
private void describeWholeTableShowingMismatches(Description mismatchDescription) {
if(!unmatchedMatchers().isEmpty()){
mismatchDescription.appendText(actualTable.toStringShowingFirstRows(5));
}
else if(!mismatchedRowMapping.isEmpty())
describeWholeTableCentredOnFirstMismatchRow(actualTable, mismatchDescription);
}
private void describeMismatchIfMatchesIndexCell(TableRowMatcher rowInTableMatcher, TableRow actualRow,
Description mismatchDescription) {
IndexCellMatcher indexedBy = rowInTableMatcher.indexedBy();
if(indexedBy.matches(actualRow)){
mismatchDescription.appendText("\nin row " + actualRow.index() + ":\n");
rowInTableMatcher.describeMismatch(actualRow, mismatchDescription);//TODO could fetch stored mismatch description here
}
}
public List<Matcher<TableRow>> unmatchedMatchers() {
return remainingMatchers;
}
public void findMatchFor(TableRow actualRow) {
for (Matcher<TableRow> matcher : remainingMatchers) {
if (matcher.matches(actualRow)) {
//we've satisfied the matcher - remove it from list of matchers
//TODO we could store the mismatchdescriptions at this point
remainingMatchers.remove(matcher);
return ;
}
else if (matchesOnIndexCells(actualRow, matcher)){
mismatchedRowMapping.put((TableRowMatcher) matcher, actualRow);
remainingMatchers.remove(matcher);
return;
}
}
//we've gone through all row matchers and not matched this row. 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 rows goes with which unmatched row-matcher.
}
private boolean matchesOnIndexCells(TableRow actualRow,
Matcher<TableRow> matcher) {
if(matcher instanceof TableRowMatcher){
TableRowMatcher rowInTableMatcher = (TableRowMatcher)matcher;
IndexCellMatcher indexedBy = rowInTableMatcher.indexedBy();
return indexedBy.matches(actualRow);
}
return false;
}
public boolean isFinished() {
return remainingMatchers.isEmpty() && mismatchedRowMapping.isEmpty();
}
}
@SafeVarargs
@Factory
public static Matcher<Table> hasDataRowsIncluding(Matcher<TableRow>... dataRowMatchers) {
return new IsTableIncludingRows(asList(dataRowMatchers));
}
}