You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ScreenshotTB3Test.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. /*
  2. * Copyright 2000-2014 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.tests.tb3;
  17. import java.io.File;
  18. import java.io.FileFilter;
  19. import java.io.FileNotFoundException;
  20. import java.io.IOException;
  21. import java.util.ArrayList;
  22. import java.util.List;
  23. import org.apache.commons.io.FileUtils;
  24. import org.junit.After;
  25. import org.junit.Before;
  26. import org.junit.Rule;
  27. import org.junit.rules.TestRule;
  28. import org.junit.rules.TestWatcher;
  29. import org.junit.runner.Description;
  30. import org.openqa.selenium.WebDriver;
  31. import org.openqa.selenium.WebElement;
  32. import org.openqa.selenium.remote.DesiredCapabilities;
  33. import com.vaadin.testbench.Parameters;
  34. import com.vaadin.testbench.ScreenshotOnFailureRule;
  35. import com.vaadin.testbench.parallel.BrowserUtil;
  36. import com.vaadin.testbench.screenshot.ImageFileUtil;
  37. /**
  38. * Base class which provides functionality for tests which use the automatic
  39. * screenshot comparison function.
  40. *
  41. * @author Vaadin Ltd
  42. */
  43. public abstract class ScreenshotTB3Test extends AbstractTB3Test {
  44. @Rule
  45. public ScreenshotOnFailureRule screenshotOnFailure = new ScreenshotOnFailureRule(
  46. this, true) {
  47. @Override
  48. protected void failed(Throwable throwable, Description description) {
  49. super.failed(throwable, description);
  50. closeApplication();
  51. }
  52. @Override
  53. protected void succeeded(Description description) {
  54. super.succeeded(description);
  55. closeApplication();
  56. }
  57. @Override
  58. protected File getErrorScreenshotFile(Description description) {
  59. return ImageFileUtil
  60. .getErrorScreenshotFile(getScreenshotFailureName());
  61. };
  62. };
  63. private String screenshotBaseName;
  64. @Rule
  65. public TestRule watcher = new TestWatcher() {
  66. @Override
  67. protected void starting(org.junit.runner.Description description) {
  68. Class<?> testClass = description.getTestClass();
  69. // Runner adds [BrowserName] which we do not want to use in the
  70. // screenshot name
  71. String testMethod = description.getMethodName();
  72. testMethod = testMethod.replaceAll("\\[.*\\]", "");
  73. String className = testClass.getSimpleName();
  74. screenshotBaseName = className + "-" + testMethod;
  75. }
  76. };
  77. /**
  78. * Contains a list of screenshot identifiers for which
  79. * {@link #compareScreen(String)} has failed during the test
  80. */
  81. private List<String> screenshotFailures;
  82. /**
  83. * Defines TestBench screen comparison parameters before each test run
  84. */
  85. @Before
  86. public void setupScreenComparisonParameters() {
  87. screenshotFailures = new ArrayList<String>();
  88. Parameters.setScreenshotErrorDirectory(getScreenshotErrorDirectory());
  89. Parameters
  90. .setScreenshotReferenceDirectory(getScreenshotReferenceDirectory());
  91. }
  92. /**
  93. * Grabs a screenshot and compares with the reference image with the given
  94. * identifier. Supports alternative references and will succeed if the
  95. * screenshot matches at least one of the references.
  96. *
  97. * In case of a failed comparison this method stores the grabbed screenshots
  98. * in the error directory as defined by
  99. * {@link #getScreenshotErrorDirectory()}. It will also generate a html file
  100. * in the same directory, comparing the screenshot with the first found
  101. * reference.
  102. *
  103. * @param identifier
  104. * @throws IOException
  105. */
  106. protected void compareScreen(String identifier) throws IOException {
  107. compareScreen(null, identifier);
  108. }
  109. protected void compareScreen(WebElement element, String identifier)
  110. throws IOException {
  111. if (identifier == null || identifier.isEmpty()) {
  112. throw new IllegalArgumentException("Empty identifier not supported");
  113. }
  114. File mainReference = getScreenshotReferenceFile(identifier);
  115. List<File> referenceFiles = findReferenceAndAlternatives(mainReference);
  116. List<File> failedReferenceFiles = new ArrayList<File>();
  117. for (File referenceFile : referenceFiles) {
  118. boolean match = false;
  119. if (element == null) {
  120. // Full screen
  121. match = testBench(driver).compareScreen(referenceFile);
  122. } else {
  123. // Only the element
  124. match = customTestBench(driver).compareScreen(element,
  125. referenceFile,
  126. BrowserUtil.isIE8(getDesiredCapabilities()));
  127. }
  128. if (match) {
  129. // There might be failure files because of retries in TestBench.
  130. deleteFailureFiles(getErrorFileFromReference(referenceFile));
  131. break;
  132. } else {
  133. failedReferenceFiles.add(referenceFile);
  134. }
  135. }
  136. File referenceToKeep = null;
  137. if (failedReferenceFiles.size() == referenceFiles.size()) {
  138. // Ensure we use the correct browser version (e.g. if running IE11
  139. // and only an IE 10 reference was available, then mainReference
  140. // will be for IE 10, not 11)
  141. String originalName = getScreenshotReferenceName(identifier);
  142. File exactVersionFile = new File(originalName);
  143. if (!exactVersionFile.equals(mainReference)) {
  144. // Rename png+html to have the correct version
  145. File correctPng = getErrorFileFromReference(exactVersionFile);
  146. File producedPng = getErrorFileFromReference(mainReference);
  147. File correctHtml = htmlFromPng(correctPng);
  148. File producedHtml = htmlFromPng(producedPng);
  149. producedPng.renameTo(correctPng);
  150. producedHtml.renameTo(correctHtml);
  151. referenceToKeep = exactVersionFile;
  152. screenshotFailures.add(exactVersionFile.getName());
  153. } else {
  154. // All comparisons failed, keep the main error image + HTML
  155. screenshotFailures.add(mainReference.getName());
  156. referenceToKeep = mainReference;
  157. }
  158. }
  159. // Remove all PNG/HTML files we no longer need (failed alternative
  160. // references or all error files (PNG/HTML) if comparison succeeded)
  161. for (File failedAlternative : failedReferenceFiles) {
  162. File failurePng = getErrorFileFromReference(failedAlternative);
  163. if (failedAlternative != referenceToKeep) {
  164. // Delete png + HTML
  165. deleteFailureFiles(failurePng);
  166. }
  167. }
  168. if (referenceToKeep != null) {
  169. File errorPng = getErrorFileFromReference(referenceToKeep);
  170. enableAutoswitch(new File(errorPng.getParentFile(),
  171. errorPng.getName() + ".html"));
  172. }
  173. }
  174. private CustomTestBenchCommandExecutor customTestBench = null;
  175. private CustomTestBenchCommandExecutor customTestBench(WebDriver driver) {
  176. if (customTestBench == null) {
  177. customTestBench = new CustomTestBenchCommandExecutor(driver);
  178. }
  179. return customTestBench;
  180. }
  181. private void enableAutoswitch(File htmlFile) throws FileNotFoundException,
  182. IOException {
  183. if (htmlFile == null || !htmlFile.exists()) {
  184. return;
  185. }
  186. String html = FileUtils.readFileToString(htmlFile);
  187. html = html.replace("body onclick=\"",
  188. "body onclick=\"clearInterval(autoSwitch);");
  189. html = html.replace("</script>",
  190. ";autoSwitch=setInterval(switchImage,500);</script>");
  191. FileUtils.writeStringToFile(htmlFile, html);
  192. }
  193. private void deleteFailureFiles(File failurePng) {
  194. File failureHtml = htmlFromPng(failurePng);
  195. failurePng.delete();
  196. failureHtml.delete();
  197. }
  198. /**
  199. * Returns a new File which points to a .html file instead of the given .png
  200. * file
  201. *
  202. * @param png
  203. * @return
  204. */
  205. private static File htmlFromPng(File png) {
  206. return new File(png.getParentFile(), png.getName().replaceAll(
  207. "\\.png$", ".png.html"));
  208. }
  209. /**
  210. *
  211. * @param referenceFile
  212. * The reference image file (in the directory defined by
  213. * {@link #getScreenshotReferenceDirectory()})
  214. * @return the file name of the file generated in the directory defined by
  215. * {@link #getScreenshotErrorDirectory()} if comparison with the
  216. * given reference image fails.
  217. */
  218. private File getErrorFileFromReference(File referenceFile) {
  219. String absolutePath = referenceFile.getAbsolutePath();
  220. String screenshotReferenceDirectory = getScreenshotReferenceDirectory();
  221. String screenshotErrorDirectory = getScreenshotErrorDirectory();
  222. // We throw an exception to safeguard against accidental reference
  223. // deletion. See (#14446)
  224. if (!absolutePath.contains(screenshotReferenceDirectory)) {
  225. throw new IllegalStateException(
  226. "Reference screenshot not in reference directory. Screenshot path: '"
  227. + absolutePath + "', directory path: '"
  228. + screenshotReferenceDirectory + "'");
  229. }
  230. return new File(absolutePath.replace(screenshotReferenceDirectory,
  231. screenshotErrorDirectory));
  232. }
  233. /**
  234. * Finds alternative references for the given files
  235. *
  236. * @param reference
  237. * @return all references which should be considered when comparing with the
  238. * given files, including the given reference
  239. */
  240. private List<File> findReferenceAndAlternatives(File reference) {
  241. List<File> files = new ArrayList<File>();
  242. files.add(reference);
  243. File screenshotDir = reference.getParentFile();
  244. String name = reference.getName();
  245. // Remove ".png"
  246. String nameBase = name.substring(0, name.length() - 4);
  247. for (int i = 1;; i++) {
  248. File file = new File(screenshotDir, nameBase + "_" + i + ".png");
  249. if (file.exists()) {
  250. files.add(file);
  251. } else {
  252. break;
  253. }
  254. }
  255. return files;
  256. }
  257. /**
  258. * @param testName
  259. * @return the reference file name to use for the given browser, as
  260. * described by {@literal capabilities}, and identifier
  261. */
  262. private File getScreenshotReferenceFile(String identifier) {
  263. DesiredCapabilities capabilities = getDesiredCapabilities();
  264. String originalName = getScreenshotReferenceName(identifier);
  265. File exactVersionFile = new File(originalName);
  266. if (exactVersionFile.exists()) {
  267. return exactVersionFile;
  268. }
  269. String browserVersion = capabilities.getVersion();
  270. // compare against screenshots for this version and older
  271. // default such that if no particular version is requested, compare with
  272. // any version
  273. int maxVersion = 100;
  274. if (browserVersion.matches("\\d+")) {
  275. maxVersion = Integer.parseInt(browserVersion);
  276. }
  277. for (int version = maxVersion; version > 0; version--) {
  278. String fileName = getScreenshotReferenceName(identifier, version);
  279. File oldVersionFile = new File(fileName);
  280. if (oldVersionFile.exists()) {
  281. return oldVersionFile;
  282. }
  283. }
  284. return exactVersionFile;
  285. }
  286. /**
  287. * @return the base directory of 'reference' and 'errors' screenshots
  288. */
  289. protected abstract String getScreenshotDirectory();
  290. /**
  291. * @return the base directory of 'reference' and 'errors' screenshots with a
  292. * trailing file separator
  293. */
  294. private String getScreenshotDirectoryWithTrailingSeparator() {
  295. String screenshotDirectory = getScreenshotDirectory();
  296. if (!screenshotDirectory.endsWith(File.separator)) {
  297. screenshotDirectory += File.separator;
  298. }
  299. return screenshotDirectory;
  300. }
  301. /**
  302. * @return the directory where reference images are stored (the 'reference'
  303. * folder inside the screenshot directory)
  304. */
  305. private String getScreenshotReferenceDirectory() {
  306. return getScreenshotDirectoryWithTrailingSeparator() + "reference";
  307. }
  308. /**
  309. * @return the directory where comparison error images should be created
  310. * (the 'errors' folder inside the screenshot directory)
  311. */
  312. private String getScreenshotErrorDirectory() {
  313. return getScreenshotDirectoryWithTrailingSeparator() + "errors";
  314. }
  315. /**
  316. * Checks if any screenshot comparisons failures occurred during the test
  317. * and combines all comparison errors into one exception
  318. *
  319. * @throws IOException
  320. * If there were failures during the test
  321. */
  322. @After
  323. public void checkCompareFailures() throws IOException {
  324. if (screenshotFailures != null && !screenshotFailures.isEmpty()) {
  325. throw new IOException(
  326. "The following screenshots did not match the reference: "
  327. + screenshotFailures.toString());
  328. }
  329. }
  330. /**
  331. * @return the name of a "failure" image which is stored in the folder
  332. * defined by {@link #getScreenshotErrorDirectory()} when the test
  333. * fails
  334. */
  335. private String getScreenshotFailureName() {
  336. return getScreenshotBaseName() + "_" + getUniqueIdentifier(null)
  337. + "-failure.png";
  338. }
  339. /**
  340. * @return the base name used for screenshots. This is the first part of the
  341. * screenshot file name, typically created as "testclass-testmethod"
  342. */
  343. public String getScreenshotBaseName() {
  344. return screenshotBaseName;
  345. }
  346. /**
  347. * Returns the name of the reference file based on the given parameters.
  348. *
  349. * @param testName
  350. * @param capabilities
  351. * @param identifier
  352. * @return the full path of the reference
  353. */
  354. private String getScreenshotReferenceName(String identifier) {
  355. return getScreenshotReferenceName(identifier, null);
  356. }
  357. /**
  358. * Returns the name of the reference file based on the given parameters. The
  359. * version given in {@literal capabilities} is used unless it is overridden
  360. * by the {@literal versionOverride} parameter.
  361. *
  362. * @param testName
  363. * @param capabilities
  364. * @param identifier
  365. * @return the full path of the reference
  366. */
  367. private String getScreenshotReferenceName(String identifier,
  368. Integer versionOverride) {
  369. return getScreenshotReferenceDirectory() + File.separator
  370. + getScreenshotBaseName() + "_"
  371. + getUniqueIdentifier(versionOverride) + "_" + identifier
  372. + ".png";
  373. }
  374. private String getUniqueIdentifier(Integer versionOverride) {
  375. String testNameAndParameters = testName.getMethodName();
  376. // runTest-wildfly9-nginx[Windows_Firefox_24][/buffering/demo][valo]
  377. String parameters = testNameAndParameters.substring(
  378. testNameAndParameters.indexOf("[") + 1,
  379. testNameAndParameters.length() - 1);
  380. // Windows_Firefox_24][/buffering/demo][valo
  381. parameters = parameters.replace("][", "_");
  382. // Windows_Firefox_24_/buffering/demo_valo
  383. parameters = parameters.replace("/", "");
  384. // Windows_Firefox_24_bufferingdemo_valo
  385. if (versionOverride != null) {
  386. // Windows_Firefox_17_bufferingdemo_valo
  387. parameters = parameters.replaceFirst("_"
  388. + getDesiredCapabilities().getVersion(), "_"
  389. + versionOverride);
  390. }
  391. return parameters;
  392. }
  393. /**
  394. * Returns the base name of the screenshot in the error directory. This is a
  395. * name so that all files matching {@link #getScreenshotErrorBaseName()}*
  396. * are owned by this test instance (taking into account
  397. * {@link #getDesiredCapabilities()}) and can safely be removed before
  398. * running this test.
  399. */
  400. private String getScreenshotErrorBaseName() {
  401. return getScreenshotReferenceName("dummy", null).replace(
  402. getScreenshotReferenceDirectory(),
  403. getScreenshotErrorDirectory()).replace("_dummy.png", "");
  404. }
  405. /**
  406. * Removes any old screenshots related to this test from the errors
  407. * directory before running the test
  408. */
  409. @Before
  410. public void cleanErrorDirectory() {
  411. // Remove any screenshots for this test from the error directory
  412. // before running it. Leave unrelated files as-is
  413. File errorDirectory = new File(getScreenshotErrorDirectory());
  414. // Create errors directory if it does not exist
  415. if (!errorDirectory.exists()) {
  416. errorDirectory.mkdirs();
  417. }
  418. final String errorBase = getScreenshotErrorBaseName();
  419. File[] files = errorDirectory.listFiles(new FileFilter() {
  420. @Override
  421. public boolean accept(File pathname) {
  422. String thisFile = pathname.getAbsolutePath();
  423. if (thisFile.startsWith(errorBase)) {
  424. return true;
  425. }
  426. return false;
  427. }
  428. });
  429. for (File f : files) {
  430. f.delete();
  431. }
  432. }
  433. }