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