package com.github.andreptb.fitnesse.util;
import java.io.File;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair;
/**
* General utilities to process FitNesse markup syntax so can be used by Selenium Fixture
*/
public class FitnesseMarkup {
/**
* Markup which presents image preview and download link
*/
private static final String SCREENSHOT_LINK_MARKUP = "<a href=\"javascript:void(0)\" onclick=\"window.open(this.childNodes[0].getAttribute(''src''));\"><img src=\"data:image/png;base64,{0}\" height=\"200\"></img</a>";
/**
* @see #compare(Object, Object)
*/
private static final Pattern FITNESSE_REGEX_MARKUP_PATTERN = Pattern.compile("^=~/(.+)/");
/**
* Constant used to register selenium special keys as system properties
*/
private static final String KEYBOARD_SPECIAL_KEY_VARIABLE_MARKUP = "KEY_{0}";
/**
* Constant representing special key format. Needs !--! to properly render html markup
*/
private static final String KEYBOARD_SPECIAL_KEY_RENDERING_MARKUP = "!-<span keycode=\"{1}\">$'{'{0}'}'</span>-!";
/**
* Constant representing the value separator in selector [selector]@[atributte]->[value]
*/
public static final String SELECTOR_VALUE_SEPARATOR = "->";
/**
* Constant representing the negation flag in {@link #SELECTOR_VALUE_SEPARATOR}
*/
public static final String SELECTOR_VALUE_DENY_INDICATOR = "!";
/**
* Constants representing selector separators [selector]@[atributte]$[value]
*/
public static final String SELECTOR_ATTRIBUTE_SEPARATOR = "@";
/**
* Constant representing type separator [type]=[value]
*/
public static final String KEY_VALUE_SEPARATOR = "=";
/**
* Constant representing [width]x[height] 'x' separator
*/
private static final String WIDTH_HEIGHT_SEPARATOR = "x";
private static final Pattern WIDTH_HEIGHT_PATTERN = Pattern.compile("(\\d{1,4})x(\\d{1,4})");
/**
* <b>on</b> value constant, see {@link #booleanToOnOrOff(Object)}
*/
public static final String ON_VALUE = "on";
/**
* <b>off</b> value constant, see {@link #booleanToOnOrOff(Object)}
*/
public static final String OFF_VALUE = "off";
/**
* Constant representing an exception message contained within a failure
*/
private static final String EXCEPTION_MESSAGE_MARKUP = "screenshot:<<{0}>>, message:<<{1}>>";
private static final Pattern SCREENSHOT_WITHIN_EXCEPTION_PATTERN = Pattern.compile("screenshot:<<([^>]+)>>");
/**
* Compares two values emulating FitNesse comparisons:
* <p>
* http://fitnesse.org/FitNesse.FullReferenceGuide.UserGuide.WritingAcceptanceTests.SliM.ValueComparisons
* </p>
* For now supports only exact equal and regular expression comparisons
*
* @param expected value
* @param obtained value
* @return comparisonResult
*/
public boolean compare(Object expected, Object obtained) {
String cleanedExpected = clean(expected);
String cleanedObtained = clean(obtained);
boolean not = StringUtils.startsWith(cleanedExpected, FitnesseMarkup.SELECTOR_VALUE_DENY_INDICATOR);
cleanedExpected = StringUtils.stripStart(cleanedExpected, FitnesseMarkup.SELECTOR_VALUE_DENY_INDICATOR);
Matcher matcher = FitnesseMarkup.FITNESSE_REGEX_MARKUP_PATTERN.matcher(cleanedExpected);
boolean result = false;
if (matcher.matches()) {
result = Pattern.compile(matcher.group(NumberUtils.INTEGER_ONE), Pattern.DOTALL).matcher(cleanedObtained).matches();
} else {
result = StringUtils.equals(cleanedExpected, cleanedObtained);
}
return not ? !result : result;
}
/**
* Cleans FitNesse markup from symbols such as:
* <ul>
* <li>Extracts a keyboard special key value from special key markup. See #</li>
* <li>Extracts URL only from HTML generated links</li>
* <li>Extracts text from HTML wiki page creation suggestion link</li>
* <li>Strips undefined variable ocurrences on text</li>
* <li>If value is associated with</li>
* </ul>
*
* @param symbol to be cleaned
* @return cleanedSymbol
*/
public String clean(Object symbol) {
// strips whitespace and accidental "null" string representation of null value
String strippedSymbol = StringUtils.remove(StringUtils.strip(ObjectUtils.toString(symbol)), "null");
if (StringUtils.isBlank(strippedSymbol)) {
return strippedSymbol;
}
// transforms keyboard special keys markup
strippedSymbol = strippedSymbol.replaceAll("<span keycode=\"([^\"]+)\"[^/]+/span>", "$1");
// removes undefined variable references
strippedSymbol = strippedSymbol.replaceAll("<span[^>]+>undefined variable:[^<]+</span>", StringUtils.EMPTY);
// removes create wikipage markup
strippedSymbol = strippedSymbol.replaceAll("<a[^>]+>\\[\\?\\]</a>", StringUtils.EMPTY);
// removes undefined variable references
strippedSymbol = strippedSymbol.replaceAll("<span[^>]+>undefined variable:[^<]+</span>", StringUtils.EMPTY);
// removes html tags
return strippedSymbol.replaceAll("</?.[^>]+>", StringUtils.EMPTY);
}
/**
* Creates img markup to be viewed in test page.
* Usually used by fixtures that wants to return a image link for the test result.
*
* @param img
* File containing the image
* @return Image link
*/
public String imgLink(Object img) {
return MessageFormat.format(FitnesseMarkup.SCREENSHOT_LINK_MARKUP, img);
}
/**
* Creates img markup to be viewed in test page. Extracts the image content from an exception message generated by {@link #exceptionMessage(Object, String, Object...)}
*
* @param exceptionMessage
* to be parsed
* @return image link
*/
public String imgLinkFromExceptionMessage(String exceptionMessage) {
Matcher matcher = FitnesseMarkup.SCREENSHOT_WITHIN_EXCEPTION_PATTERN.matcher(exceptionMessage);
if (matcher.find()) {
return imgLink(matcher.group(NumberUtils.INTEGER_ONE));
}
return null;
}
/**
* Registers a system property ({@link System#setProperty(String, String)}) allowing user to develop tests referencing special keys such as tab and enter by using variables. For example:
* <p>
* If <b>keyName="tab"</b> and <b>keyValue="\uE004"</b> then <b>${KEY_TAB}</b> will resolve to <b>"<span keycode="\uE004">tab</span>"</b>
* </p>
*
* @param keyName Special keyboard key name
* @param keyValue Special keyboard key value
*/
public void registerKeyboardSpecialKey(String keyName, String keyValue) {
String generatedKeyName = MessageFormat.format(FitnesseMarkup.KEYBOARD_SPECIAL_KEY_VARIABLE_MARKUP, StringUtils.upperCase(keyName));
System.setProperty(generatedKeyName, MessageFormat.format(FitnesseMarkup.KEYBOARD_SPECIAL_KEY_RENDERING_MARKUP, generatedKeyName, keyValue));
}
public Pair<String, String> swapValueToCheck(String stringWithValue, String stringToGetValue) {
String expectedValue = StringUtils.substringAfterLast(stringWithValue, FitnesseMarkup.SELECTOR_VALUE_SEPARATOR);
return Pair.of(StringUtils.substringBeforeLast(stringWithValue, FitnesseMarkup.SELECTOR_VALUE_SEPARATOR), stringToGetValue + FitnesseMarkup.SELECTOR_VALUE_SEPARATOR + expectedValue);
}
/**
* @param value file path parts
* @return normalized a file path, cleaning each path part and joining with current operating system separator
*/
public File cleanFile(Object... value) {
String[] cleanedValues = Arrays.stream(value).map(valueToClean -> FilenameUtils.normalize(clean(valueToClean))).toArray(size -> new String[value.length]);
return FileUtils.getFile(cleanedValues);
}
/**
* Cleans and split the value in [key][separator][value] format.
*
* @param value to parse, should be in [key][separator][value]
* @param separator to be used
* @return instance of {@link Pair} containing key and value
*/
public Pair<String, String> cleanAndParseKeyValue(Object value, String separator) {
String cleanedValue = clean(value);
if (!StringUtils.contains(cleanedValue, separator)) {
return Pair.of(cleanedValue, StringUtils.EMPTY);
}
String key = StringUtils.substringBefore(cleanedValue, separator);
return Pair.of(key, StringUtils.removeStart(cleanedValue, key + separator));
}
/**
* Cleans and converts <b>true</b> or <b>false</b> value to {@link #ON_VALUE} or {@link #OFF_VALUE}
*
* @param value to be converted
* @return converted value, {@link #ON_VALUE} if value is <b>true</b> or {@link #ON_VALUE} itself
*/
public String booleanToOnOrOff(Object value) {
return onOrOffToBoolean(value) ? FitnesseMarkup.ON_VALUE : FitnesseMarkup.OFF_VALUE;
}
/**
* Cleans and converts {@link #ON_VALUE} or {@link #OFF_VALUE} value to boolean.
*
* @param value to be converted
* @return converted value, <b>true</b> if value is {@link #ON_VALUE}, <b>false</b> otherwise
*/
public boolean onOrOffToBoolean(Object value) {
String cleanedValue = clean(value);
return Boolean.valueOf(cleanedValue) || StringUtils.equalsIgnoreCase(cleanedValue, FitnesseMarkup.ON_VALUE);
}
/**
* @param width Numeric value representing a width value
* @param height Numeric value representing a height value
* @return formatted width and height. If width=1024 and height=768, will output 1024x768
*/
public String formatWidthAndHeight(Object width, Object height) {
return width + FitnesseMarkup.WIDTH_HEIGHT_SEPARATOR + height;
}
/**
* @param widthAndHeight {@link String} containing width and height, separated by {@link #WIDTH_HEIGHT_SEPARATOR}. Example: 1920x1080, 1280x720.
* @return instance of {@link Pair} containing width in {@link Pair#getLeft()} and height in {@link Pair#getRight()}
*/
public Pair<Integer, Integer> parseWidthAndHeight(String widthAndHeight) {
String cleanedWidthAndHeight = clean(widthAndHeight);
Matcher matcher = FitnesseMarkup.WIDTH_HEIGHT_PATTERN.matcher(cleanedWidthAndHeight);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid width and height format, should be something like [width pixels]x[height pixels]. Obtained: " + widthAndHeight);
}
return Pair.of(NumberUtils.toInt(matcher.group(1)), NumberUtils.toInt(matcher.group(2)));
}
/**
* Produces a custom SLIM response containing the original exception message along with a screenshot, probably taken when the exception occured.
*
* @param originalMessage
* message taken from the exception
* @param screenshotData
* screenshot data in base64 format
* @param args
* extra arguments to be interpolated in {@link MessageFormat#format(Object)}
* @return formatted message
*/
public String exceptionMessage(Object originalMessage, String screenshotData, Object... args) {
String originalMessageString = clean(originalMessage);
try {
return MessageFormat.format(FitnesseMarkup.EXCEPTION_MESSAGE_MARKUP, screenshotData, MessageFormat.format(originalMessageString, args));
} catch (IllegalArgumentException e) {
return MessageFormat.format(FitnesseMarkup.EXCEPTION_MESSAGE_MARKUP, screenshotData, originalMessageString);
}
}
}