package com.github.andreptb.fitnesse.plugins; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ClassUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.commons.lang3.reflect.MethodUtils; import com.github.andreptb.fitnesse.SeleniumFixture; import com.github.andreptb.fitnesse.util.FitnesseMarkup; import fitnesse.slim.instructions.CallInstruction; import fitnesse.slim.instructions.ImportInstruction; import fitnesse.slim.instructions.Instruction; import fitnesse.testsystems.TestResult; import fitnesse.testsystems.slim.SlimTestContext; import fitnesse.testsystems.slim.Table; import fitnesse.testsystems.slim.results.SlimExceptionResult; import fitnesse.testsystems.slim.tables.ScriptTable; import fitnesse.testsystems.slim.tables.SlimAssertion; import fitnesse.testsystems.slim.tables.SlimExpectation; /** * Selenium table, works just like ScriptTable, but adds extra features such as step screenshots and provide extra information to the fixture, allowing wait behavior for check and ensure actions */ public class SeleniumScriptTable extends ScriptTable { /** * JUL Logger instance */ private static final Logger LOGGER = Logger.getLogger(SeleniumScriptTable.class.getName()); /** * Table keyword constant */ public static final String TABLE_KEYWORD = "selenium"; /** * Table type constant */ private static final String TABLE_TYPE = SeleniumScriptTable.TABLE_KEYWORD + "Script"; /** * Fixture package to auto-import package */ private static final String SELENIUM_FIXTURE_PACKAGE_TO_IMPORT = "com.github.andreptb.fitnesse"; /** * Constant to reference {@link CallInstruction} args private field */ private static final String CALL_INSTRUCTION_METHODNAME_FIELD = "methodName"; /** * Constant to reference {@link CallInstruction} args private field */ private static final String CALL_INSTRUCTION_ARGS_FIELD = "args"; /** * Utility to process FitNesse markup */ private FitnesseMarkup fitnesseMarkup = new FitnesseMarkup(); public SeleniumScriptTable(Table table, String id, SlimTestContext testContext) { super(table, id, testContext); } @Override protected String getTableType() { return SeleniumScriptTable.TABLE_TYPE; } @Override protected String getTableKeyword() { return SeleniumScriptTable.TABLE_KEYWORD; } /** * Overrides start actor to force the use of Selenium Fixture. Auto imports selenium fixture if needed */ @Override protected List<SlimAssertion> startActor() { List<SlimAssertion> assertions = new ArrayList<>(); assertions.add(makeAssertion(new ImportInstruction(ImportInstruction.INSTRUCTION, SeleniumScriptTable.SELENIUM_FIXTURE_PACKAGE_TO_IMPORT), SlimExpectation.NOOP_EXPECTATION)); assertions.addAll(startActor(NumberUtils.INTEGER_ZERO, SeleniumFixture.class.getName(), NumberUtils.INTEGER_ZERO)); return assertions; } @Override protected List<SlimAssertion> ensure(int row) { List<SlimAssertion> assertions = super.ensure(row); injectValueInFirstArg(assertions, false, true); return assertions; } @Override protected List<SlimAssertion> reject(int row) { List<SlimAssertion> assertions = super.reject(row); injectValueInFirstArg(assertions, false, false); return assertions; } @Override protected List<SlimAssertion> checkAction(int row) { List<SlimAssertion> assertions = super.checkAction(row); injectResultToAction(row, assertions, false); return assertions; } @Override protected List<SlimAssertion> checkNotAction(int row) { List<SlimAssertion> assertions = super.checkNotAction(row); injectResultToAction(row, assertions, true); return assertions; } private void injectResultToAction(int row, List<SlimAssertion> assertions, boolean not) { String contentToCheck = this.fitnesseMarkup.clean(this.table.getCellContents(this.table.getColumnCountInRow(row) - 1, row)); if (CollectionUtils.isEmpty(assertions) || StringUtils.isBlank(contentToCheck)) { return; } injectValueInFirstArg(assertions, not, contentToCheck); } private void injectValueInFirstArg(List<SlimAssertion> assertions, boolean not, Object contentToCheck) { SlimAssertion.getInstructions(assertions).forEach(instruction -> { try { String valueToInject = FitnesseMarkup.SELECTOR_VALUE_SEPARATOR + (not ? FitnesseMarkup.SELECTOR_VALUE_DENY_INDICATOR : StringUtils.EMPTY) + contentToCheck; Object args = FieldUtils.readField(instruction, SeleniumScriptTable.CALL_INSTRUCTION_ARGS_FIELD, true); Object[] argsToInject; if (args instanceof Object[] && ArrayUtils.getLength(args) > NumberUtils.INTEGER_ZERO) { argsToInject = (Object[]) args; argsToInject[NumberUtils.INTEGER_ZERO] += valueToInject; } else { argsToInject = ArrayUtils.toArray(valueToInject); } String methodName = Objects.toString(FieldUtils.readField(instruction, SeleniumScriptTable.CALL_INSTRUCTION_METHODNAME_FIELD, true)); if (Objects.isNull(MethodUtils.getAccessibleMethod(SeleniumFixture.class, Objects.toString(methodName), ClassUtils.toClass(argsToInject)))) { SeleniumScriptTable.LOGGER.fine("Method for instruction not found on SeleniumFixture, injection aborted: " + instruction); return; } FieldUtils.writeField(instruction, SeleniumScriptTable.CALL_INSTRUCTION_ARGS_FIELD, argsToInject, true); } catch (IllegalArgumentException | ReflectiveOperationException e) { SeleniumScriptTable.LOGGER.log(Level.FINE, "Failed to inject check value using reflection", e); } }); } @Override protected List<SlimAssertion> actionAndAssign(String symbolName, int row) { return super.actionAndAssign(symbolName, row).stream().map(assertion -> { Optional<Instruction> instruction = SlimAssertion.getInstructions(Arrays.asList(assertion)).stream().findFirst(); if(instruction.isPresent()) { ScreenshotEmbedderSlimExpectation expectation = new ScreenshotEmbedderSlimExpectation(assertion.getExpectation()); return super.makeAssertion(instruction.get(), expectation); } return assertion; }).collect(Collectors.toList()); } @Override protected List<SlimAssertion> invokeAction(int startingCol, int endingCol, int row, SlimExpectation expectation) { return super.invokeAction(startingCol, endingCol, row, new ScreenshotEmbedderSlimExpectation(expectation)); } private class ScreenshotEmbedderSlimExpectation implements SlimExpectation { private SlimExpectation original; ScreenshotEmbedderSlimExpectation(SlimExpectation original) { this.original = original; } @Override public TestResult evaluateExpectation(Object returnValues) { return this.original.evaluateExpectation(returnValues); } @Override public SlimExceptionResult evaluateException(SlimExceptionResult exceptionResult) { SlimExceptionResult result = this.original.evaluateException(exceptionResult); if(this.original instanceof RowExpectation) { String screenshot = SeleniumScriptTable.this.fitnesseMarkup.imgLinkFromExceptionMessage(exceptionResult.getException()); if(StringUtils.isNotBlank(screenshot)) { SeleniumScriptTable.this.getTable().addColumnToRow(((RowExpectation) this.original).getRow(), screenshot); } } return result; } } }