├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── core ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── kreyssel │ └── selenium │ └── visualdiff │ └── core │ ├── ScreenshotManager.java │ ├── ScreenshotMeta.java │ ├── ScreenshotStore.java │ ├── diff │ ├── ImageCompare.java │ ├── VisualDiff.java │ ├── VisualDiffMeta.java │ └── VisualDiffMetaGrouper.java │ └── junit4 │ └── TakesScreenshotRule.java ├── it ├── pom.xml └── src │ ├── main │ └── webapp │ │ ├── WEB-INF │ │ └── web.xml │ │ ├── different.jsp │ │ ├── equal.jsp │ │ └── index.jsp │ └── test │ └── java │ └── org │ └── kreyssel │ └── selenium │ └── visualdiff │ └── it │ ├── AmazonIT.java │ ├── GoogleIT.java │ └── SimpleSeleniumIT.java ├── maven-plugin ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── kreyssel │ └── selenium │ └── visualdiff │ └── mojo │ ├── BaseMojo.java │ ├── PackageMojo.java │ ├── PrepareMojo.java │ ├── ReportMojo.java │ └── report │ ├── VisualDiffReportRenderer.java │ ├── VisualDiffReportUtil.java │ └── VisualDiffTestReportRenderer.java └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .settings 3 | .classpath 4 | target -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk7 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Selenium Visual Diff 2 | ==================== 3 | 4 | Goal 5 | ---- 6 | The goal is a better integration of screenshots taking in maven executed *selenium2* 7 | functional tests, storing and versioning of screenshots to get a report of visual differences 8 | between two application versions. 9 | 10 | Report 11 | ------ 12 | See sample report http://kreyssel.github.com/selenium2-visualdiff/visualdiff.html. 13 | 14 | Usage 15 | ----- 16 | Embed *jUnit4* and the *selenium2-visualdiff* core library as dependencies in your funtional test maven module: 17 | 18 | 19 | ... 20 | 21 | org.kreyssel.selenium2.visualdiff 22 | visualdiff-core 23 | 1.0.0-SNAPSHOT 24 | test 25 | 26 | ... 27 | 28 | 29 | Add the selenium2-visualdiff-maven-plugin to the maven module: 30 | 31 | 32 | ... 33 | 34 | org.kreyssel.selenium2.visualdiff 35 | visualdiff-maven-plugin 36 | 1.0.0-SNAPSHOT 37 | 38 | 39 | 40 | prepare 41 | package 42 | 43 | 44 | 45 | 46 | ... 47 | 48 | 49 | And after all, embed *org.kreyssel.selenium2.visualdiff.core.junit4.TakesScreenshotRule* in your functional test: 50 | 51 | package org.kreyssel.selenium2.visualdiff.it; 52 | 53 | import org.kreyssel.selenium2.visualdiff.core.junit4.TakesScreenshotRule; 54 | 55 | /** 56 | * SimpleSeleniumIT. 57 | */ 58 | public class SimpleSeleniumIT { 59 | 60 | @Rule 61 | public TakesScreenshotRule screenshot = new TakesScreenshotRule(); 62 | 63 | RemoteWebDriver driver; 64 | 65 | @Before 66 | public void init() { 67 | driver = createDriver(); 68 | } 69 | 70 | @After 71 | public void destroy() { 72 | driver.close(); 73 | } 74 | 75 | @Test 76 | public void test1() throws Exception { 77 | driver.get( "http://localhost:8080" ); 78 | 79 | screenshot.takeScreenshot( driver ); 80 | } 81 | } 82 | 83 | After the executions of functional tests in a maven run, all screenshots taken at this time are packaged as zip and attached to the build as *${project.build.finalName}-screenshots.zip*. This archive is deployed to maven repository in the *deploy* phase of the maven build. 84 | 85 | The *selenium2-visualdiff* report plugin generates a report that shows you the different screens per testcase compared to the previous release. 86 | 87 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.kreyssel.selenium2.visualdiff 8 | visualdiff-parent 9 | 1.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | 13 | visualdiff-core 14 | jar 15 | 16 | VisualDiff :: Core Library 17 | Module with core functionalities to easily create screenshots. 18 | 19 | 20 | 21 | 22 | org.seleniumhq.selenium 23 | selenium-api 24 | provided 25 | 26 | 27 | 28 | 29 | de.schlichtherle.truezip 30 | truezip-file 31 | 7.2.1 32 | 33 | 34 | de.schlichtherle.truezip 35 | truezip-driver-file 36 | 7.2.1 37 | 38 | 39 | de.schlichtherle.truezip 40 | truezip-driver-zip 41 | 7.2.1 42 | 43 | 44 | 45 | 46 | org.apache.commons 47 | commons-lang3 48 | 49 | 50 | commons-io 51 | commons-io 52 | 53 | 54 | 55 | junit 56 | junit 57 | provided 58 | 59 | 60 | 61 | 62 | org.seleniumhq.selenium 63 | selenium-java 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.codehaus.mojo 73 | versions-maven-plugin 74 | 1.2 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/ScreenshotManager.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.HashSet; 7 | import java.util.Properties; 8 | import java.util.Set; 9 | 10 | import org.apache.commons.io.IOUtils; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.openqa.selenium.OutputType; 13 | import org.openqa.selenium.TakesScreenshot; 14 | import org.openqa.selenium.WebDriver; 15 | 16 | /** 17 | * ScreenshotManager. 18 | */ 19 | public final class ScreenshotManager { 20 | 21 | public static final String PROPERTIES_FILE = "screenshotmanager.properties"; 22 | 23 | public static final String PROPERTY_OUTPUT_FILEPATH = "outputpath"; 24 | 25 | private final Class testClass; 26 | 27 | private final String testMethodName; 28 | 29 | private final ScreenshotStore screenshotStore; 30 | 31 | private final Set screenshotIds; 32 | 33 | /** 34 | * Creates a new ScreenshotManager object. 35 | * 36 | * @throws IOException 37 | */ 38 | public ScreenshotManager(final Class testClass, final String testMethodName) { 39 | this.testClass = testClass; 40 | this.testMethodName = testMethodName; 41 | try { 42 | screenshotStore = new ScreenshotStore(getScreenshotArchivePath(testClass)); 43 | } catch (IOException ex) { 44 | throw new RuntimeException("Error on get archive filepath!", ex); 45 | } 46 | this.screenshotIds = new HashSet(); 47 | } 48 | 49 | /** 50 | * Take screenshot. 51 | * 52 | * @param screenshotId 53 | * the screenshot id 54 | * @param driver 55 | * the driver 56 | * @return the file 57 | * @throws IOException 58 | * Signals that an I/O exception has occurred. 59 | */ 60 | public ScreenshotMeta takeScreenshot(final WebDriver driver, final String screenshotId) 61 | throws IOException { 62 | 63 | validateScreenshotId(screenshotId); 64 | 65 | if (!(driver instanceof TakesScreenshot)) { 66 | throw new RuntimeException("Class '" + driver.getClass().getName() 67 | + "' is not a instance of '" + TakesScreenshot.class.getName() + "'!"); 68 | } 69 | 70 | String url = driver.getCurrentUrl(); 71 | String title = driver.getTitle(); 72 | 73 | TakesScreenshot screenshotDriver = (TakesScreenshot) driver; 74 | 75 | File tmpPngFile = screenshotDriver.getScreenshotAs(OutputType.FILE); 76 | 77 | String screenshotSignature = testClass.getName() + "#"+testMethodName + ":"+screenshotId; 78 | 79 | if (tmpPngFile == null) { 80 | throw new RuntimeException("Got no screenshot for test '" + screenshotSignature + "'!"); 81 | } else if (tmpPngFile.length() < 1) { 82 | throw new RuntimeException("Screenshot for test '" + screenshotSignature 83 | + "' is 0 byte!"); 84 | } 85 | 86 | ScreenshotMeta meta = screenshotStore.addScreenshot(testClass.getName(), testMethodName, screenshotId, url, title, tmpPngFile); 87 | 88 | return meta; 89 | } 90 | 91 | 92 | 93 | /** 94 | * DOCUMENT ME! 95 | * 96 | * @param testClass 97 | * classLoader DOCUMENT ME! 98 | * @param screenshotSignature 99 | * screenshotId DOCUMENT ME! 100 | * 101 | * @return DOCUMENT ME! 102 | * 103 | * @throws IOException 104 | * DOCUMENT ME! 105 | */ 106 | String getScreenshotArchivePath(final String screenshotSignature, String fileEnding) throws IOException { 107 | String filepath = screenshotSignature.replace('.', '/') + fileEnding; 108 | 109 | return filepath; 110 | } 111 | 112 | /** 113 | * DOCUMENT ME! 114 | * 115 | * @param testClass 116 | * classLoader testClass DOCUMENT ME! 117 | * 118 | * @return DOCUMENT ME! 119 | * 120 | * @throws IOException 121 | * DOCUMENT ME! 122 | */ 123 | private File getScreenshotArchivePath(final Class locatorClass) throws IOException { 124 | Properties props = new Properties(); 125 | InputStream in = null; 126 | 127 | try { 128 | String filepath = getPropertiesFilePath(); 129 | 130 | in = locatorClass.getResourceAsStream(filepath); 131 | 132 | if (in == null) { 133 | throw new IOException("Could not found '" + filepath + "'!"); 134 | } 135 | 136 | props.load(in); 137 | } finally { 138 | IOUtils.closeQuietly(in); 139 | } 140 | 141 | String outputPath = props.getProperty(PROPERTY_OUTPUT_FILEPATH); 142 | 143 | if (StringUtils.isBlank(outputPath)) { 144 | throw new RuntimeException("Could not found value for property '" 145 | + PROPERTY_OUTPUT_FILEPATH + "' in file '" + PROPERTIES_FILE + "'!"); 146 | } 147 | 148 | return new File(outputPath); 149 | } 150 | 151 | /** 152 | * DOCUMENT ME! 153 | * 154 | * @return DOCUMENT ME! 155 | */ 156 | public static String getPropertiesFilePath() { 157 | String classPath = ScreenshotManager.class.getPackage().getName().replace('.', '/'); 158 | 159 | String filepath = "/" + classPath + "/" + PROPERTIES_FILE; 160 | 161 | return filepath; 162 | } 163 | 164 | /** 165 | * DOCUMENT ME! 166 | * 167 | * @param screenshotId 168 | * DOCUMENT ME! 169 | */ 170 | void validateScreenshotId(final String screenshotId) { 171 | 172 | if (!screenshotId.matches("[a-zA-Z0-9]+")) { 173 | throw new RuntimeException("Wrong screenshot id format '" + screenshotId + "'!"); 174 | } 175 | 176 | if (this.screenshotIds.contains(screenshotId)) { 177 | throw new RuntimeException("Duplicate screenshot id '" + screenshotId + "'!"); 178 | } 179 | 180 | this.screenshotIds.add(screenshotId); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/ScreenshotMeta.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.Properties; 7 | 8 | import org.apache.commons.lang3.builder.EqualsBuilder; 9 | import org.apache.commons.lang3.builder.HashCodeBuilder; 10 | import org.apache.commons.lang3.builder.ToStringBuilder; 11 | 12 | public class ScreenshotMeta { 13 | 14 | public final String testClass; 15 | 16 | public final String testMethod; 17 | 18 | public final String screenshotId; 19 | 20 | public final String url; 21 | 22 | public final String title; 23 | 24 | public final String filepath; 25 | 26 | protected ScreenshotMeta(final String testClass, final String testMethod, 27 | final String screenshotId, final String url, final String title, final String filepath) { 28 | this.testClass = testClass; 29 | this.testMethod = testMethod; 30 | this.screenshotId = screenshotId; 31 | this.url = url; 32 | this.title = title; 33 | this.filepath = filepath; 34 | } 35 | 36 | protected ScreenshotMeta(final ScreenshotMeta screenshotMeta) { 37 | this.testClass = screenshotMeta.testClass; 38 | this.testMethod = screenshotMeta.testMethod; 39 | this.screenshotId = screenshotMeta.screenshotId; 40 | this.url = screenshotMeta.url; 41 | this.title = screenshotMeta.title; 42 | this.filepath = screenshotMeta.filepath; 43 | } 44 | 45 | public void store(final OutputStream out) throws IOException { 46 | Properties props = new Properties(); 47 | props.setProperty("testClass", testClass); 48 | props.setProperty("testMethod", testMethod); 49 | props.setProperty("screenshotId", screenshotId); 50 | props.setProperty("url", url); 51 | props.setProperty("title", title); 52 | props.setProperty("filepath", filepath); 53 | props.store(out, ""); 54 | } 55 | 56 | public static ScreenshotMeta load(final InputStream in) throws IOException { 57 | Properties props = new Properties(); 58 | props.load(in); 59 | 60 | return new ScreenshotMeta(props.getProperty("testClass"), props.getProperty("testMethod"), 61 | props.getProperty("screenshotId"), props.getProperty("url"), 62 | props.getProperty("title"), props.getProperty("filepath")); 63 | } 64 | 65 | @Override 66 | public boolean equals(final Object obj) { 67 | if (obj == null) { 68 | return false; 69 | } else if (!(obj instanceof ScreenshotMeta)) { 70 | return false; 71 | } 72 | 73 | ScreenshotMeta compMeta = (ScreenshotMeta) obj; 74 | 75 | return new EqualsBuilder().append(testClass, compMeta.testClass) 76 | .append(testMethod, compMeta.testMethod) 77 | .append(screenshotId, compMeta.screenshotId).isEquals(); 78 | } 79 | 80 | @Override 81 | public int hashCode() { 82 | return new HashCodeBuilder().append(testClass).append(testMethod).append(screenshotId) 83 | .hashCode(); 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return ToStringBuilder.reflectionToString(this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/ScreenshotStore.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core; 2 | 3 | import java.io.File; 4 | import java.io.FileFilter; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import de.schlichtherle.truezip.file.TFile; 11 | import de.schlichtherle.truezip.file.TFileInputStream; 12 | import de.schlichtherle.truezip.file.TFileOutputStream; 13 | 14 | public class ScreenshotStore { 15 | 16 | private TFile archive; 17 | 18 | public ScreenshotStore(final File storeFile) { 19 | this.archive = new TFile(storeFile); 20 | } 21 | 22 | public ScreenshotMeta addScreenshot(final String testClass, final String testMethod, 23 | final String screenshotId, final String url, final String title, final File file) 24 | throws IOException { 25 | String pngPath = getPath(testClass, testMethod, screenshotId, ".png"); 26 | String propPath = getPath(testClass, testMethod, screenshotId, ".properties"); 27 | 28 | TFile fileInArchive = new TFile(this.archive, pngPath); 29 | 30 | // copy screenshot file to archive 31 | new TFile(file).cp(fileInArchive); 32 | 33 | ScreenshotMeta meta = new ScreenshotMeta(testClass, testMethod, screenshotId, url, title, 34 | fileInArchive.getInnerEntryName()); 35 | 36 | TFileOutputStream out = new TFileOutputStream(new TFile(this.archive, propPath)); 37 | try { 38 | meta.store(out); 39 | } finally { 40 | out.close(); 41 | } 42 | 43 | return meta; 44 | } 45 | 46 | public List getScreenshots() throws IOException { 47 | ArrayList metaList = new ArrayList(); 48 | 49 | readMeta(metaList, archive.listFiles(new PropertiesFileFilter())); 50 | 51 | return metaList; 52 | } 53 | 54 | public InputStream getInputStream(final String filepath) throws IOException { 55 | return new TFileInputStream(new TFile(archive, filepath)); 56 | } 57 | 58 | public void copy(final String filepath, final File file) throws IOException { 59 | file.getParentFile().mkdirs(); 60 | new TFile(archive, filepath).cp(file); 61 | } 62 | 63 | protected void readMeta(final ArrayList metaList, final TFile[] entries) 64 | throws IOException { 65 | for (TFile file : entries) { 66 | if (file.isDirectory()) { 67 | readMeta(metaList, file.listFiles()); 68 | } else if (file.getName().endsWith(".properties")) { 69 | metaList.add(loadMeta(file)); 70 | } 71 | } 72 | } 73 | 74 | protected ScreenshotMeta loadMeta(final TFile file) throws IOException { 75 | ScreenshotMeta meta; 76 | 77 | TFileInputStream in = new TFileInputStream(file); 78 | try { 79 | meta = ScreenshotMeta.load(in); 80 | } finally { 81 | in.close(); 82 | } 83 | 84 | return meta; 85 | } 86 | 87 | protected String getPath(final String testClass, final String testMethod, 88 | final String screenshotId, final String fileEnding) { 89 | return (testClass + "/" + testMethod + "_" + screenshotId).replace('.', '/') + fileEnding; 90 | } 91 | 92 | /** 93 | * class PropertiesFileFilter 94 | * 95 | * @author kreyssel 96 | */ 97 | private static class PropertiesFileFilter implements FileFilter { 98 | 99 | public boolean accept(final File file) { 100 | return file.isDirectory() || file.getName().endsWith(".properties"); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/diff/ImageCompare.java: -------------------------------------------------------------------------------- 1 | /************************************************************************* 2 | * 3 | * OpenOffice.org - a multi-platform office productivity suite 4 | * 5 | * $RCSfile: ScreenComparer.java,v $ 6 | * 7 | * $Revision: 1.3 $ 8 | * 9 | * last change: $Author: rt $ $Date: 2005/10/19 11:54:12 $ 10 | * 11 | * The Contents of this file are made available subject to 12 | * the terms of GNU Lesser General Public License Version 2.1. 13 | * 14 | * 15 | * GNU Lesser General Public License Version 2.1 16 | * ============================================= 17 | * Copyright 2005 by Sun Microsystems, Inc. 18 | * 901 San Antonio Road, Palo Alto, CA 94303, USA 19 | * 20 | * This library is free software; you can redistribute it and/or 21 | * modify it under the terms of the GNU Lesser General Public 22 | * License version 2.1, as published by the Free Software Foundation. 23 | * 24 | * This library is distributed in the hope that it will be useful, 25 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 26 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 27 | * Lesser General Public License for more details. 28 | * 29 | * You should have received a copy of the GNU Lesser General Public 30 | * License along with this library; if not, write to the Free Software 31 | * Foundation, Inc., 59 Temple Place, Suite 330, Boston, 32 | * MA 02111-1307 USA 33 | * 34 | ************************************************************************/ 35 | package org.kreyssel.selenium.visualdiff.core.diff; 36 | 37 | import java.awt.image.BufferedImage; 38 | import java.awt.image.PixelGrabber; 39 | import java.io.File; 40 | import java.io.FileOutputStream; 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.io.OutputStream; 44 | 45 | import javax.imageio.ImageIO; 46 | 47 | /** 48 | * 49 | * 50 | *

51 | * This implementation based on 52 | * http://ooo.googlecode.com/svn/trunk/bean/qa/complex/ScreenComparer.java 53 | * 54 | * 55 | * @author kreyssel 56 | * 57 | */ 58 | public class ImageCompare { 59 | 60 | private int diffColor; 61 | 62 | private BufferedImage img1; 63 | private BufferedImage img2; 64 | private BufferedImage imgDiff; 65 | 66 | public ImageCompare(final InputStream in1, final InputStream in2) throws IOException { 67 | int red = 0xff; 68 | int alpha = 0xff; 69 | diffColor = (alpha << 24); 70 | diffColor = diffColor | (red << 16); 71 | 72 | img1 = ImageIO.read(in1); 73 | img2 = ImageIO.read(in2); 74 | } 75 | 76 | public boolean compare() throws InterruptedException { 77 | 78 | boolean ret = true; 79 | int w1 = img1.getWidth(); 80 | int h1 = img1.getHeight(); 81 | int w2 = img2.getWidth(); 82 | int h2 = img2.getHeight(); 83 | 84 | if (w1 != w2 || h1 != h2) { 85 | // Different size. Create an image that holds both images. 86 | int w = w1 > w2 ? w1 : w2; 87 | int h = h1 > h2 ? h1 : h2; 88 | imgDiff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); 89 | for (int y = 0; y < h; y++) { 90 | for (int x = 0; x < w; x++) { 91 | boolean bOutOfRange = false; 92 | int pixel1 = 0; 93 | int pixel2 = 0; 94 | // get the pixel for m_img1 95 | if (x < w1 && y < h1) 96 | pixel1 = img1.getRGB(x, y); 97 | else 98 | bOutOfRange = true; 99 | 100 | if (x < w2 && y < h2) 101 | pixel2 = img2.getRGB(x, y); 102 | else 103 | bOutOfRange = true; 104 | 105 | if (bOutOfRange || pixel1 != pixel2) 106 | imgDiff.setRGB(x, y, diffColor); 107 | else 108 | imgDiff.setRGB(x, y, pixel1); 109 | 110 | } 111 | } 112 | return false; 113 | } 114 | 115 | // Images have same dimension 116 | int[] pixels1 = new int[w1 * h1]; 117 | PixelGrabber pg = new PixelGrabber(img1.getSource(), 0, 0, w1, h1, pixels1, 0, w1); 118 | pg.grabPixels(); 119 | 120 | int[] pixels2 = new int[w2 * h2]; 121 | PixelGrabber pg2 = new PixelGrabber(img2.getSource(), 0, 0, w2, h2, pixels2, 0, w2); 122 | pg2.grabPixels(); 123 | 124 | imgDiff = new BufferedImage(w1, h1, BufferedImage.TYPE_INT_ARGB); 125 | 126 | // First check if the the images differ. 127 | int lenAr = pixels1.length; 128 | int index = 0; 129 | for (index = 0; index < lenAr; index++) { 130 | if (pixels1[index] != pixels2[index]) 131 | break; 132 | } 133 | 134 | // If the images are different, then create the diff image 135 | if (index < lenAr) { 136 | for (int y = 0; y < h1; y++) { 137 | for (int x = 0; x < w1; x++) { 138 | int offset = y * w1 + x; 139 | if (pixels1[offset] != pixels2[offset]) { 140 | ret = ret && false; 141 | imgDiff.setRGB(x, y, diffColor); 142 | } else { 143 | imgDiff.setRGB(x, y, pixels1[offset]); 144 | } 145 | } 146 | } 147 | } 148 | return ret; 149 | } 150 | 151 | public void saveDiffAsPng(final File file) throws IOException { 152 | FileOutputStream out = new FileOutputStream(file); 153 | try { 154 | saveDiffAsPng(out); 155 | } finally { 156 | out.close(); 157 | } 158 | } 159 | 160 | public void saveDiffAsPng(final OutputStream out) throws IOException { 161 | ImageIO.write(imgDiff, "png", out); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/diff/VisualDiff.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core.diff; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import org.apache.commons.io.IOUtils; 10 | import org.kreyssel.selenium.visualdiff.core.ScreenshotMeta; 11 | import org.kreyssel.selenium.visualdiff.core.ScreenshotStore; 12 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta.Type; 13 | 14 | public class VisualDiff { 15 | 16 | private File outputDirectory; 17 | 18 | public VisualDiff(final File outputDirectory) { 19 | this.outputDirectory = outputDirectory; 20 | } 21 | 22 | public List diff(final ScreenshotStore store1, final ScreenshotStore store2) 23 | throws IOException { 24 | List screenshots1 = store1.getScreenshots(); 25 | List screenshots2 = store2.getScreenshots(); 26 | 27 | ArrayList diffs = new ArrayList(); 28 | diff(diffs, store1, screenshots1, store2, screenshots2, Type.ADDED); 29 | diff(diffs, store2, screenshots2, store1, screenshots1, Type.REMOVED); 30 | 31 | return diffs; 32 | } 33 | 34 | protected void diff(final List diffMeta, final ScreenshotStore store1, 35 | final List screenshots1, final ScreenshotStore store2, 36 | final List screenshots2, final Type diffMode) throws IOException { 37 | 38 | for (ScreenshotMeta screenshot1 : screenshots1) { 39 | if (diffMeta.contains(screenshot1)) { 40 | continue; 41 | } 42 | 43 | Type innerDiffType = diffMode; 44 | 45 | ScreenshotMeta screenshot2 = null; 46 | ImageCompare ic = null; 47 | 48 | if (screenshots2.contains(screenshot1)) { 49 | screenshot2 = screenshots2.get(screenshots2.indexOf(screenshot1)); 50 | 51 | ic = createImageCompare(store1, store2, screenshot1, screenshot2); 52 | 53 | try { 54 | innerDiffType = ic.compare() ? Type.EQUAL : Type.DIFFERENT; 55 | } catch (InterruptedException ex) { 56 | throw new IOException("Error on compare screenshots!", ex); 57 | } 58 | } else { 59 | innerDiffType = diffMode; 60 | } 61 | 62 | VisualDiffMeta vdMeta = new VisualDiffMeta(screenshot1, innerDiffType); 63 | diffMeta.add(vdMeta); 64 | 65 | File sc1File = new File(outputDirectory, vdMeta.getScreenshot1Filepath()); 66 | store1.copy(screenshot1.filepath, sc1File); 67 | 68 | if (innerDiffType == Type.DIFFERENT) { 69 | File sc2File = new File(outputDirectory, vdMeta.getScreenshot2Filepath()); 70 | store2.copy(screenshot2.filepath, sc2File); 71 | 72 | File diffFile = new File(outputDirectory, vdMeta.getScreenshotDiffFilepath()); 73 | ic.saveDiffAsPng(diffFile); 74 | } 75 | } 76 | } 77 | 78 | private ImageCompare createImageCompare(final ScreenshotStore store1, 79 | final ScreenshotStore store2, final ScreenshotMeta screenshot1, 80 | final ScreenshotMeta screenshot2) throws IOException { 81 | 82 | ImageCompare ic; 83 | 84 | InputStream in1 = null; 85 | InputStream in2 = null; 86 | 87 | try { 88 | in1 = store1.getInputStream(screenshot1.filepath); 89 | in2 = store2.getInputStream(screenshot2.filepath); 90 | ic = new ImageCompare(in1, in2); 91 | } finally { 92 | IOUtils.closeQuietly(in1); 93 | IOUtils.closeQuietly(in2); 94 | } 95 | return ic; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/diff/VisualDiffMeta.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core.diff; 2 | 3 | import java.io.File; 4 | 5 | import org.kreyssel.selenium.visualdiff.core.ScreenshotMeta; 6 | 7 | public class VisualDiffMeta extends ScreenshotMeta { 8 | 9 | public final Type diffType; 10 | 11 | public enum Type { 12 | EQUAL, ADDED, REMOVED, DIFFERENT; 13 | } 14 | 15 | protected VisualDiffMeta(final ScreenshotMeta screenshotMeta, final Type diffType) { 16 | super(screenshotMeta); 17 | this.diffType = diffType; 18 | } 19 | 20 | public String getScreenshot1Filepath() { 21 | return getFile("New"); 22 | } 23 | 24 | public String getScreenshot2Filepath() { 25 | return getFile("Old"); 26 | } 27 | 28 | public String getScreenshotDiffFilepath() { 29 | return getFile("Diff"); 30 | } 31 | 32 | private String getFile(final String suffix) { 33 | File file = new File(this.filepath); 34 | String name = file.getName(); 35 | int idx = name.lastIndexOf('.'); 36 | String newName = name.substring(0, idx) + "_" + suffix + name.substring(idx); 37 | 38 | return new File(file.getParent(), newName).getPath(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/diff/VisualDiffMetaGrouper.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core.diff; 2 | 3 | import java.util.List; 4 | 5 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta.Type; 6 | 7 | import com.google.common.base.Function; 8 | import com.google.common.collect.ImmutableListMultimap; 9 | import com.google.common.collect.Multimaps; 10 | 11 | /** 12 | * Grouped by test-class, test-method, screenshot-id 13 | * 14 | * @author conny 15 | * 16 | */ 17 | public final class VisualDiffMetaGrouper { 18 | 19 | private VisualDiffMetaGrouper() { 20 | } 21 | 22 | public static ImmutableListMultimap byTestClass( 23 | final List diffs) { 24 | 25 | ImmutableListMultimap byTest = Multimaps.index(diffs, 26 | new Function() { 27 | public String apply(final VisualDiffMeta input) { 28 | return input.testClass; 29 | } 30 | }); 31 | 32 | return byTest; 33 | } 34 | 35 | public static ImmutableListMultimap byTestMethod( 36 | final List diffsPerTest) { 37 | 38 | ImmutableListMultimap byMethod = Multimaps.index(diffsPerTest, 39 | new Function() { 40 | public String apply(final VisualDiffMeta input) { 41 | return input.testMethod; 42 | } 43 | }); 44 | 45 | return byMethod; 46 | } 47 | 48 | public static int countByDiffType(final List diffs, final Type diffType) { 49 | int count = 0; 50 | for (VisualDiffMeta diff : diffs) { 51 | if (diff.diffType == diffType) { 52 | count++; 53 | } 54 | } 55 | return count; 56 | } 57 | } -------------------------------------------------------------------------------- /core/src/main/java/org/kreyssel/selenium/visualdiff/core/junit4/TakesScreenshotRule.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.core.junit4; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import org.junit.rules.MethodRule; 7 | import org.junit.runners.model.FrameworkMethod; 8 | import org.junit.runners.model.Statement; 9 | import org.kreyssel.selenium.visualdiff.core.ScreenshotManager; 10 | import org.kreyssel.selenium.visualdiff.core.ScreenshotMeta; 11 | import org.openqa.selenium.WebDriver; 12 | 13 | /** 14 | * TakesScreenshotRule. 15 | */ 16 | public class TakesScreenshotRule implements MethodRule { 17 | 18 | private ScreenshotManager screenshotManager; 19 | 20 | /** 21 | * DOCUMENT ME! 22 | * 23 | * @param base 24 | * DOCUMENT ME! 25 | * @param method 26 | * DOCUMENT ME! 27 | * @param target 28 | * DOCUMENT ME! 29 | * 30 | * @return DOCUMENT ME! 31 | */ 32 | public Statement apply(final Statement base, final FrameworkMethod method, final Object target) { 33 | 34 | this.screenshotManager = new ScreenshotManager(target.getClass(), method.getName()); 35 | 36 | return base; 37 | } 38 | 39 | /** 40 | * DOCUMENT ME! 41 | * 42 | * @param driver 43 | * DOCUMENT ME! 44 | * 45 | * @return DOCUMENT ME! 46 | * 47 | * @throws IOException 48 | */ 49 | public ScreenshotMeta takeScreenshot(final WebDriver driver) throws IOException { 50 | return takeScreenshot("1", driver); 51 | } 52 | 53 | /** 54 | * DOCUMENT ME! 55 | * 56 | * @param id 57 | * DOCUMENT ME! 58 | * @param driver 59 | * DOCUMENT ME! 60 | * 61 | * @return DOCUMENT ME! 62 | * 63 | * @throws IOException 64 | */ 65 | public ScreenshotMeta takeScreenshot(final String id, final WebDriver driver) throws IOException { 66 | return screenshotManager.takeScreenshot(driver, id); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /it/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.kreyssel.selenium2.visualdiff 8 | visualdiff-parent 9 | 1.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | 13 | visualdiff-it 14 | 1.0.2-SNAPSHOT 15 | war 16 | 17 | VisualDiff :: Integration Tests 18 | 19 | 20 | 21 | ${project.groupId} 22 | visualdiff-core 23 | ${project.parent.version} 24 | 25 | 26 | 27 | org.seleniumhq.selenium 28 | selenium-java 29 | test 30 | 31 | 32 | 33 | junit 34 | junit 35 | test 36 | 37 | 38 | 39 | 40 | 41 | 42 | ${project.groupId} 43 | visualdiff-maven-plugin 44 | 1.0.0-SNAPSHOT 45 | 46 | 47 | 48 | prepare 49 | package 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.mortbay.jetty 57 | jetty-maven-plugin 58 | 59 | 0 60 | 54321 61 | STOP 62 | 63 | 64 | 65 | start-server 66 | pre-integration-test 67 | 68 | run 69 | 70 | 71 | true 72 | 73 | 74 | 75 | stop-server 76 | post-integration-test 77 | 78 | stop 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-failsafe-plugin 87 | 2.9 88 | 89 | 90 | integration-test 91 | 92 | integration-test 93 | 94 | 95 | 96 | verify 97 | 98 | verify 99 | 100 | 101 | 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-install-plugin 107 | 2.3.1 108 | 109 | true 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | ${project.groupId} 119 | visualdiff-maven-plugin 120 | 1.0.0-SNAPSHOT 121 | 122 | 123 | 124 | visualdiff-report 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /it/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /it/src/main/webapp/different.jsp: -------------------------------------------------------------------------------- 1 | <%@page import="java.util.Date"%> 2 | <%@page import="java.text.SimpleDateFormat"%> 3 | <%@ page language="java" contentType="text/html; charset=ISO-8859-1" 4 | pageEncoding="ISO-8859-1"%> 5 | 6 | 7 | 8 | 9 | Insert title here 10 | 11 | 12 | <%= new SimpleDateFormat("HH:mm:ss").format(new Date()) %> 13 | 14 | -------------------------------------------------------------------------------- /it/src/main/webapp/equal.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=ISO-8859-1" 2 | pageEncoding="ISO-8859-1"%> 3 | 4 | 5 | 6 | 7 | Insert title here 8 | 9 | 10 | Test --------------------- changed 11 | 12 | -------------------------------------------------------------------------------- /it/src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=ISO-8859-1" 2 | pageEncoding="ISO-8859-1"%> 3 | 4 | 5 | 6 | 7 | Insert title here 8 | 9 | 10 | Test 11 | 12 | -------------------------------------------------------------------------------- /it/src/test/java/org/kreyssel/selenium/visualdiff/it/AmazonIT.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.it; 2 | 3 | import org.apache.commons.lang3.SystemUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.kreyssel.selenium.visualdiff.core.junit4.TakesScreenshotRule; 9 | import org.openqa.selenium.By; 10 | import org.openqa.selenium.WebDriver; 11 | import org.openqa.selenium.firefox.FirefoxDriver; 12 | import org.openqa.selenium.ie.InternetExplorerDriver; 13 | import org.openqa.selenium.remote.RemoteWebDriver; 14 | import org.openqa.selenium.support.ui.ExpectedCondition; 15 | import org.openqa.selenium.support.ui.WebDriverWait; 16 | 17 | /** 18 | * SimpleSeleniumIT. 19 | */ 20 | public class AmazonIT { 21 | 22 | @Rule 23 | public TakesScreenshotRule screenshot = new TakesScreenshotRule(); 24 | 25 | RemoteWebDriver driver; 26 | WebDriverWait wait; 27 | 28 | @Before 29 | public void init() { 30 | driver = createDriver(); 31 | wait = new WebDriverWait(driver, 30); 32 | } 33 | 34 | @After 35 | public void destroy() { 36 | driver.close(); 37 | } 38 | 39 | @Test 40 | public void startPage() throws Exception { 41 | driver.get("http://www.amazon.com"); 42 | 43 | screenshot.takeScreenshot(driver); 44 | } 45 | 46 | @Test 47 | public void searchBooks() throws Exception { 48 | driver.get("http://www.amazon.com"); 49 | 50 | screenshot.takeScreenshot("startPage", driver); 51 | 52 | driver.findElementById("twotabsearchtextbox").sendKeys("selenium2"); 53 | 54 | screenshot.takeScreenshot("afterInput", driver); 55 | 56 | driver.findElementById("navGoButton").findElement(By.tagName("input")).click(); 57 | 58 | wait.until(new ExpectedCondition() { 59 | public Boolean apply(final WebDriver webDriver) { 60 | System.out.println("searching ..."); 61 | return webDriver.findElement(By.id("atfResults")) != null; 62 | } 63 | }); 64 | 65 | screenshot.takeScreenshot("afterSearch", driver); 66 | } 67 | 68 | private RemoteWebDriver createDriver() { 69 | if (SystemUtils.IS_OS_WINDOWS) 70 | return new InternetExplorerDriver(); 71 | 72 | return new FirefoxDriver(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /it/src/test/java/org/kreyssel/selenium/visualdiff/it/GoogleIT.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.it; 2 | 3 | import org.apache.commons.lang3.SystemUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.kreyssel.selenium.visualdiff.core.junit4.TakesScreenshotRule; 9 | import org.openqa.selenium.By; 10 | import org.openqa.selenium.WebDriver; 11 | import org.openqa.selenium.firefox.FirefoxDriver; 12 | import org.openqa.selenium.ie.InternetExplorerDriver; 13 | import org.openqa.selenium.remote.RemoteWebDriver; 14 | import org.openqa.selenium.support.ui.ExpectedCondition; 15 | import org.openqa.selenium.support.ui.WebDriverWait; 16 | 17 | /** 18 | * SimpleSeleniumIT. 19 | */ 20 | public class GoogleIT { 21 | 22 | @Rule 23 | public TakesScreenshotRule screenshot = new TakesScreenshotRule(); 24 | 25 | RemoteWebDriver driver; 26 | WebDriverWait wait; 27 | 28 | @Before 29 | public void init() { 30 | driver = createDriver(); 31 | wait = new WebDriverWait(driver, 30); 32 | } 33 | 34 | @After 35 | public void destroy() { 36 | driver.close(); 37 | } 38 | 39 | @Test 40 | public void startPage() throws Exception { 41 | driver.get("http://www.google.com"); 42 | 43 | screenshot.takeScreenshot(driver); 44 | } 45 | 46 | @Test 47 | public void searchWeb() throws Exception { 48 | driver.get("http://www.google.com/?q=news+2011"); 49 | 50 | screenshot.takeScreenshot("edit", driver); 51 | 52 | driver.findElementByName("btnG").click(); 53 | 54 | wait.until(new ExpectedCondition() { 55 | public Boolean apply(final WebDriver webDriver) { 56 | System.out.println("searching ..."); 57 | return webDriver.findElement(By.id("resultStats")) != null; 58 | } 59 | }); 60 | 61 | screenshot.takeScreenshot("afterSubmit", driver); 62 | } 63 | 64 | @Test 65 | public void searchImages() throws Exception { 66 | driver.get("http://www.google.com/"); 67 | 68 | screenshot.takeScreenshot("open", driver); 69 | 70 | driver.findElementById("gb_2").click(); 71 | 72 | wait.until(new ExpectedCondition() { 73 | public Boolean apply(final WebDriver webDriver) { 74 | System.out.println("switch to images ..."); 75 | return webDriver.findElement(By.name("q")) != null; 76 | } 77 | }); 78 | 79 | screenshot.takeScreenshot("afterSwitchToImages", driver); 80 | 81 | driver.findElementByName("q").sendKeys("selenium-2\n"); 82 | 83 | wait.until(new ExpectedCondition() { 84 | public Boolean apply(final WebDriver webDriver) { 85 | System.out.println("searching ..."); 86 | return webDriver.findElement(By.id("resultStats")) != null; 87 | } 88 | }); 89 | 90 | screenshot.takeScreenshot("searchResult", driver); 91 | } 92 | 93 | private RemoteWebDriver createDriver() { 94 | if (SystemUtils.IS_OS_WINDOWS) 95 | return new InternetExplorerDriver(); 96 | 97 | return new FirefoxDriver(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /it/src/test/java/org/kreyssel/selenium/visualdiff/it/SimpleSeleniumIT.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.it; 2 | 3 | import org.apache.commons.lang3.SystemUtils; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.kreyssel.selenium.visualdiff.core.junit4.TakesScreenshotRule; 9 | import org.openqa.selenium.firefox.FirefoxDriver; 10 | import org.openqa.selenium.ie.InternetExplorerDriver; 11 | import org.openqa.selenium.remote.RemoteWebDriver; 12 | 13 | /** 14 | * SimpleSeleniumIT. 15 | */ 16 | public class SimpleSeleniumIT { 17 | 18 | @Rule 19 | public TakesScreenshotRule screenshot = new TakesScreenshotRule(); 20 | 21 | RemoteWebDriver driver; 22 | 23 | @Before 24 | public void init() { 25 | driver = createDriver(); 26 | } 27 | 28 | @After 29 | public void destroy() { 30 | driver.close(); 31 | } 32 | 33 | @Test 34 | public void testEqual() throws Exception { 35 | driver.get("http://localhost:8080/equal.jsp"); 36 | 37 | screenshot.takeScreenshot(driver); 38 | } 39 | 40 | @Test 41 | public void testDifferent() throws Exception { 42 | driver.get("http://localhost:8080/different.jsp"); 43 | 44 | screenshot.takeScreenshot(driver); 45 | } 46 | 47 | @Test 48 | public void test3() throws Exception { 49 | driver.get("http://localhost:8080"); 50 | 51 | screenshot.takeScreenshot(driver); 52 | } 53 | 54 | private RemoteWebDriver createDriver() { 55 | if (SystemUtils.IS_OS_WINDOWS) 56 | return new InternetExplorerDriver(); 57 | 58 | return new FirefoxDriver(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /maven-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.kreyssel.selenium2.visualdiff 8 | visualdiff-parent 9 | 1.0.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | 13 | visualdiff-maven-plugin 14 | maven-plugin 15 | 16 | VisualDiff :: Maven Plugin 17 | Maven plugin to add screenshots as artifact for deploying and create reports with visual diffs. 18 | 19 | 20 | 21 | org.apache.maven 22 | maven-plugin-api 23 | ${maven.version} 24 | 25 | 26 | 27 | org.apache.maven 28 | maven-core 29 | ${maven.version} 30 | provided 31 | 32 | 33 | 34 | org.apache.maven.reporting 35 | maven-reporting-api 36 | ${maven.version} 37 | 38 | 39 | 40 | org.apache.maven.reporting 41 | maven-reporting-impl 42 | 2.1 43 | 44 | 45 | 46 | org.codehaus.plexus 47 | plexus-utils 48 | 3.0 49 | 50 | 51 | 52 | org.codehaus.mojo 53 | versions-maven-plugin 54 | 1.2 55 | 56 | 57 | 58 | com.google.guava 59 | guava 60 | r09 61 | 62 | 63 | 64 | ${project.groupId} 65 | visualdiff-core 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/BaseMojo.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo; 2 | 3 | import java.io.File; 4 | 5 | import org.apache.maven.plugin.AbstractMojo; 6 | import org.apache.maven.project.MavenProject; 7 | import org.apache.maven.project.MavenProjectHelper; 8 | 9 | /** 10 | * BaseMojo for all project mojos 11 | */ 12 | public abstract class BaseMojo extends AbstractMojo { 13 | 14 | /** 15 | * Maven Project 16 | * 17 | * @parameter default-value="${project}" 18 | * @required 19 | * @readonly 20 | */ 21 | MavenProject mavenProject; 22 | 23 | /** 24 | * Maven ProjectHelper. 25 | * 26 | * @component 27 | * @readonly 28 | */ 29 | MavenProjectHelper projectHelper; 30 | 31 | /** 32 | * @parameter default-value= 33 | * "${project.build.directory}/${project.build.finalName}-screenshots.zip" 34 | * 35 | * @required 36 | */ 37 | File archiveFile; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/PackageMojo.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo; 2 | 3 | import org.apache.maven.plugin.MojoExecutionException; 4 | import org.apache.maven.plugin.MojoFailureException; 5 | 6 | /** 7 | * PackageMojo. 8 | * 9 | * @goal package 10 | * @phase post-integration-test 11 | */ 12 | public class PackageMojo extends BaseMojo { 13 | 14 | public void execute() throws MojoExecutionException, MojoFailureException { 15 | 16 | // attach screenshots zip to store in repository 17 | projectHelper.attachArtifact(mavenProject, "zip", "screenshots", archiveFile); 18 | 19 | getLog().info("Screenshots archive stored as " + archiveFile.getAbsolutePath()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/PrepareMojo.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo; 2 | 3 | import java.io.File; 4 | import java.io.FileWriter; 5 | import java.io.IOException; 6 | import java.util.Properties; 7 | 8 | import org.apache.maven.plugin.MojoExecutionException; 9 | import org.apache.maven.plugin.MojoFailureException; 10 | import org.kreyssel.selenium.visualdiff.core.ScreenshotManager; 11 | 12 | /** 13 | * PrepareMojo. 14 | * 15 | * @goal prepare 16 | * @phase validate 17 | */ 18 | public class PrepareMojo extends BaseMojo { 19 | 20 | public void execute() throws MojoExecutionException, MojoFailureException { 21 | 22 | if (mavenProject == null) { 23 | throw new MojoFailureException("project is null!"); 24 | } 25 | 26 | String testOutputDir = mavenProject.getBuild().getTestOutputDirectory(); 27 | File propertyFile = new File(testOutputDir, ScreenshotManager.getPropertiesFilePath()); 28 | 29 | try { 30 | save(propertyFile, archiveFile); 31 | } catch (IOException ex) { 32 | throw new MojoExecutionException("Error on store screenshot filepath properties file!", ex); 33 | } 34 | } 35 | 36 | /** 37 | * DOCUMENT ME! 38 | * 39 | * @param propertiesFile 40 | * DOCUMENT ME! 41 | * @param screenshotDir 42 | * DOCUMENT ME! 43 | * 44 | * @throws IOException 45 | */ 46 | private void save(final File propertiesFile, final File screenshotArchive) throws IOException { 47 | 48 | propertiesFile.getParentFile().mkdirs(); 49 | 50 | Properties props = new Properties(); 51 | props.setProperty(ScreenshotManager.PROPERTY_OUTPUT_FILEPATH, 52 | screenshotArchive.getAbsolutePath()); 53 | 54 | FileWriter w = new FileWriter(propertiesFile); 55 | 56 | try { 57 | props.store(w, "stored by " + PrepareMojo.class.getName()); 58 | } finally { 59 | w.close(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/ReportMojo.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.List; 6 | import java.util.Locale; 7 | 8 | import org.apache.maven.artifact.Artifact; 9 | import org.apache.maven.artifact.factory.ArtifactFactory; 10 | import org.apache.maven.artifact.metadata.ArtifactMetadataRetrievalException; 11 | import org.apache.maven.artifact.metadata.ArtifactMetadataSource; 12 | import org.apache.maven.artifact.repository.ArtifactRepository; 13 | import org.apache.maven.artifact.resolver.ArtifactNotFoundException; 14 | import org.apache.maven.artifact.resolver.ArtifactResolutionException; 15 | import org.apache.maven.artifact.resolver.ArtifactResolver; 16 | import org.apache.maven.artifact.versioning.ArtifactVersion; 17 | import org.apache.maven.doxia.sink.Sink; 18 | import org.apache.maven.doxia.siterenderer.Renderer; 19 | import org.apache.maven.project.MavenProject; 20 | import org.apache.maven.reporting.AbstractMavenReport; 21 | import org.apache.maven.reporting.MavenReportException; 22 | import org.codehaus.mojo.versions.api.ArtifactVersions; 23 | import org.codehaus.mojo.versions.ordering.VersionComparators; 24 | import org.kreyssel.selenium.visualdiff.core.ScreenshotStore; 25 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiff; 26 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta; 27 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMetaGrouper; 28 | import org.kreyssel.selenium.visualdiff.mojo.report.VisualDiffReportRenderer; 29 | import org.kreyssel.selenium.visualdiff.mojo.report.VisualDiffReportUtil; 30 | import org.kreyssel.selenium.visualdiff.mojo.report.VisualDiffTestReportRenderer; 31 | 32 | import com.google.common.collect.ImmutableListMultimap; 33 | 34 | /** 35 | * ReportMojo generates a report of visual diffs between two selenium2 36 | * functional tests runs. 37 | * 38 | * @goal visualdiff-report 39 | * @phase site 40 | */ 41 | public class ReportMojo extends AbstractMavenReport { 42 | 43 | /** 44 | * Directory where reports will go. 45 | * 46 | * @parameter expression="${project.reporting.outputDirectory}" 47 | * @required 48 | * @readonly 49 | */ 50 | private String outputDirectory; 51 | 52 | /** 53 | * @parameter default-value="${project}" 54 | * @required 55 | * @readonly 56 | */ 57 | private MavenProject project; 58 | 59 | /** 60 | * Used to look up Artifacts in the remote repository. 61 | * 62 | * @parameter expression= 63 | * "${component.org.apache.maven.artifact.factory.ArtifactFactory}" 64 | * @required 65 | * @readonly 66 | */ 67 | protected ArtifactFactory artifactFactory; 68 | 69 | /** 70 | * @component 71 | * @since 1.0-alpha-3 72 | */ 73 | private ArtifactResolver artifactResolver; 74 | 75 | /** 76 | * The artifact metadata source to use. 77 | * 78 | * @component 79 | * @required 80 | * @readonly 81 | */ 82 | private ArtifactMetadataSource artifactMetadataSource; 83 | 84 | /** 85 | * @parameter expression="${project.remoteArtifactRepositories}" 86 | * @readonly 87 | */ 88 | protected List remoteArtifactRepositories; 89 | 90 | /** 91 | * @parameter expression="${localRepository}" 92 | * @readonly 93 | */ 94 | protected ArtifactRepository localRepository; 95 | 96 | /** 97 | * @component 98 | * @required 99 | * @readonly 100 | */ 101 | private Renderer siteRenderer; 102 | 103 | public String getDescription(final Locale arg0) { 104 | return "desc"; 105 | } 106 | 107 | public String getName(final Locale arg0) { 108 | return "Selenium2 Visuall Diff"; 109 | } 110 | 111 | public String getOutputName() { 112 | return "visualdiff"; 113 | } 114 | 115 | @Override 116 | protected String getOutputDirectory() { 117 | return outputDirectory; 118 | } 119 | 120 | @Override 121 | protected MavenProject getProject() { 122 | return project; 123 | } 124 | 125 | @Override 126 | protected Renderer getSiteRenderer() { 127 | return siteRenderer; 128 | } 129 | 130 | @Override 131 | protected void executeReport(final Locale arg0) throws MavenReportException { 132 | 133 | Artifact artifact = project.getArtifact(); 134 | 135 | Artifact currentArtifact; 136 | try { 137 | currentArtifact = resolveScreenshotArtifact(artifact, project.getVersion()); 138 | } catch (Exception ex) { 139 | throw new MavenReportException( 140 | "Error on resolve screenshot artifact for current project!", ex); 141 | } 142 | 143 | if (currentArtifact == null) { 144 | throw new MavenReportException( 145 | "Could not found screenshot archive! Did you ensure that you run the package goal before?"); 146 | } 147 | 148 | Artifact previousArtifact; 149 | try { 150 | previousArtifact = getScreenshotsFromLatestRelease(artifact); 151 | } catch (Exception ex) { 152 | throw new MavenReportException( 153 | "Error on resolve screenshot artifact for latest project!", ex); 154 | } 155 | 156 | if (previousArtifact == null || previousArtifact.getFile() == null) { 157 | getLog().warn( 158 | "Could not found a previous release version of artifact '" 159 | + project.getArtifact() + "'!"); 160 | return; 161 | } 162 | 163 | ScreenshotStore currentScreenshotsStore = new ScreenshotStore(currentArtifact.getFile()); 164 | ScreenshotStore previousScreenshotsStore = new ScreenshotStore( 165 | previousArtifact.getFile()); 166 | VisualDiff vd = new VisualDiff(new File(outputDirectory, "images/visualdiff")); 167 | 168 | // render overview 169 | getLog().info("Render visual diff overview ..."); 170 | 171 | List diffs; 172 | try { 173 | diffs = vd.diff(currentScreenshotsStore, previousScreenshotsStore); 174 | } catch (IOException ex) { 175 | throw new MavenReportException("Error on diff screenshots!", ex); 176 | } 177 | 178 | new VisualDiffReportRenderer(getSink(), currentArtifact, previousArtifact, diffs) 179 | .render(); 180 | 181 | // render report per testclass 182 | ImmutableListMultimap groupedPerTest = VisualDiffMetaGrouper 183 | .byTestClass(diffs); 184 | for (String testClass : groupedPerTest.keySet()) { 185 | getLog().info("Render visual diff result for test '" + testClass + "' ..."); 186 | 187 | try { 188 | Sink sinkForTestClass = getSinkFactory().createSink(new File(getOutputDirectory()), 189 | VisualDiffReportUtil.asFilename(testClass, ".html")); 190 | 191 | new VisualDiffTestReportRenderer(sinkForTestClass, testClass, 192 | groupedPerTest.get(testClass)).render(); 193 | } catch (IOException ex) { 194 | getLog().error("Could not create visual diff report for test '" + testClass + "'!", 195 | ex); 196 | } 197 | } 198 | } 199 | 200 | protected Artifact getScreenshotsFromLatestRelease(final Artifact artifact) 201 | throws ArtifactMetadataRetrievalException, ArtifactResolutionException, 202 | ArtifactNotFoundException { 203 | 204 | // resolve previous version of screenshots.zip via maven metadata 205 | ArtifactVersions artifactVersions = new ArtifactVersions(artifact, 206 | artifactMetadataSource.retrieveAvailableVersions(artifact, localRepository, 207 | remoteArtifactRepositories), 208 | VersionComparators.getVersionComparator("maven")); 209 | ArtifactVersion newestArtifactVersion = artifactVersions.getNewestVersion(null, null); 210 | 211 | if (newestArtifactVersion == null) { 212 | return null; 213 | } 214 | 215 | return resolveScreenshotArtifact(artifact, newestArtifactVersion.toString()); 216 | } 217 | 218 | protected Artifact resolveScreenshotArtifact(final Artifact artifact, final String version) 219 | throws ArtifactResolutionException, ArtifactNotFoundException { 220 | 221 | // resolve screenshots.zip artifact 222 | Artifact resolveArtifact = artifactFactory.createArtifactWithClassifier( 223 | artifact.getGroupId(), artifact.getArtifactId(), version, "zip", "screenshots"); 224 | 225 | artifactResolver.resolve(resolveArtifact, remoteArtifactRepositories, localRepository); 226 | 227 | return resolveArtifact; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/report/VisualDiffReportRenderer.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo.report; 2 | 3 | import java.util.List; 4 | 5 | import org.apache.maven.artifact.Artifact; 6 | import org.apache.maven.doxia.sink.Sink; 7 | import org.apache.maven.reporting.AbstractMavenReportRenderer; 8 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta; 9 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMetaGrouper; 10 | import org.kreyssel.selenium.visualdiff.mojo.report.VisualDiffReportUtil.Depth; 11 | 12 | import com.google.common.collect.ImmutableListMultimap; 13 | 14 | public class VisualDiffReportRenderer extends AbstractMavenReportRenderer { 15 | 16 | final Artifact currentArtifact; 17 | final Artifact previousArtifact; 18 | final List diffs; 19 | 20 | public VisualDiffReportRenderer(final Sink sink, final Artifact currentArtifact, 21 | final Artifact previousArtifact, final List diffs) { 22 | super(sink); 23 | this.currentArtifact = currentArtifact; 24 | this.previousArtifact = previousArtifact; 25 | this.diffs = diffs; 26 | } 27 | 28 | @Override 29 | public String getTitle() { 30 | return "Selenium2 VisualDiff"; 31 | } 32 | 33 | @Override 34 | protected void renderBody() { 35 | sink.section1(); 36 | sink.sectionTitle1(); 37 | sink.text("Overview"); 38 | sink.sectionTitle1_(); 39 | sink.section1_(); 40 | 41 | sink.text("This is the result of the screenshots compare of the current version '" 42 | + currentArtifact.getVersion() + "' and the latest version '" 43 | + previousArtifact.getVersion() + "'."); 44 | 45 | ImmutableListMultimap perTest = VisualDiffMetaGrouper 46 | .byTestClass(diffs); 47 | VisualDiffReportUtil.renderTable(sink, Depth.CLASS, "Test Classes Overview", perTest); 48 | 49 | for (String testClass : perTest.keySet()) { 50 | ImmutableListMultimap perMethod = VisualDiffMetaGrouper 51 | .byTestMethod(perTest.get(testClass)); 52 | VisualDiffReportUtil.renderTable(sink, Depth.METHOD, "Test " + testClass, perMethod); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/report/VisualDiffReportUtil.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo.report; 2 | 3 | import org.apache.maven.doxia.sink.Sink; 4 | import org.apache.maven.doxia.util.DoxiaUtils; 5 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta; 6 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta.Type; 7 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMetaGrouper; 8 | 9 | import com.google.common.collect.ImmutableListMultimap; 10 | 11 | public final class VisualDiffReportUtil { 12 | 13 | public enum Depth { 14 | CLASS, METHOD, SCRENSHOT; 15 | } 16 | 17 | static void renderTable(final Sink sink, final Depth depth, final String title, 18 | final ImmutableListMultimap grouping) { 19 | 20 | sink.section2(); 21 | sink.sectionTitle2(); 22 | sink.text(title); 23 | sink.sectionTitle2_(); 24 | 25 | sink.table(); 26 | 27 | renderTableHeaders(sink); 28 | 29 | for (String testElement : grouping.keySet()) { 30 | VisualDiffMeta first = grouping.get(testElement).get(0); 31 | 32 | sink.tableRow(); 33 | 34 | sink.tableCell(); 35 | 36 | String anchor = depth == Depth.CLASS ? "" : "Test Method " + testElement; 37 | sink.link(VisualDiffReportUtil.asFilename(first.testClass, ".html") + "#" 38 | + DoxiaUtils.encodeId(anchor, true)); 39 | sink.text(testElement); 40 | sink.link_(); 41 | 42 | sink.tableCell_(); 43 | 44 | sink.tableCell(); 45 | sink.text(Integer.toString(grouping.get(testElement).size())); 46 | sink.tableCell_(); 47 | 48 | sink.tableCell(); 49 | sink.text(Integer.toString(VisualDiffMetaGrouper.countByDiffType( 50 | grouping.get(testElement), Type.EQUAL))); 51 | sink.tableCell_(); 52 | 53 | sink.tableCell(); 54 | sink.text(Integer.toString(VisualDiffMetaGrouper.countByDiffType( 55 | grouping.get(testElement), Type.DIFFERENT))); 56 | sink.tableCell_(); 57 | 58 | sink.tableCell(); 59 | sink.text(Integer.toString(VisualDiffMetaGrouper.countByDiffType( 60 | grouping.get(testElement), Type.ADDED))); 61 | sink.tableCell_(); 62 | 63 | sink.tableCell(); 64 | sink.text(Integer.toString(VisualDiffMetaGrouper.countByDiffType( 65 | grouping.get(testElement), Type.REMOVED))); 66 | sink.tableCell_(); 67 | 68 | sink.tableRow_(); 69 | } 70 | 71 | renderTableHeaders(sink); 72 | 73 | sink.table_(); 74 | 75 | if (depth == Depth.CLASS) 76 | sink.section1_(); 77 | else 78 | sink.section2_(); 79 | } 80 | 81 | private static void renderTableHeaders(final Sink sink) { 82 | sink.tableRow(); 83 | 84 | sink.tableHeaderCell(); 85 | sink.text("Test Class"); 86 | sink.tableHeaderCell_(); 87 | 88 | sink.tableHeaderCell(); 89 | sink.text("Total Screenshots"); 90 | sink.tableHeaderCell_(); 91 | 92 | sink.tableHeaderCell(); 93 | sink.text("Equal"); 94 | sink.tableHeaderCell_(); 95 | 96 | sink.tableHeaderCell(); 97 | sink.text("Different"); 98 | sink.tableHeaderCell_(); 99 | 100 | sink.tableHeaderCell(); 101 | sink.text("Added"); 102 | sink.tableHeaderCell_(); 103 | 104 | sink.tableHeaderCell(); 105 | sink.text("Removed"); 106 | sink.tableHeaderCell_(); 107 | 108 | sink.tableRow_(); 109 | } 110 | 111 | public static String asFilename(final String testClass, final String suffix) { 112 | return "visualdiff_" + testClass.replace('.', '_') + suffix; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /maven-plugin/src/main/java/org/kreyssel/selenium/visualdiff/mojo/report/VisualDiffTestReportRenderer.java: -------------------------------------------------------------------------------- 1 | package org.kreyssel.selenium.visualdiff.mojo.report; 2 | 3 | import java.util.List; 4 | 5 | import org.apache.maven.doxia.sink.Sink; 6 | import org.apache.maven.doxia.sink.SinkEventAttributeSet; 7 | import org.apache.maven.doxia.sink.SinkEventAttributes; 8 | import org.apache.maven.reporting.AbstractMavenReportRenderer; 9 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta; 10 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMeta.Type; 11 | import org.kreyssel.selenium.visualdiff.core.diff.VisualDiffMetaGrouper; 12 | import org.kreyssel.selenium.visualdiff.mojo.report.VisualDiffReportUtil.Depth; 13 | 14 | import com.google.common.collect.ImmutableListMultimap; 15 | 16 | public class VisualDiffTestReportRenderer extends AbstractMavenReportRenderer { 17 | 18 | final String testClass; 19 | final List diffs; 20 | 21 | public VisualDiffTestReportRenderer(final Sink sink, final String testClass, 22 | final List diffs) { 23 | super(sink); 24 | this.testClass = testClass; 25 | this.diffs = diffs; 26 | } 27 | 28 | @Override 29 | public String getTitle() { 30 | return "Selenium2 VisualDiff for Test-Class " + testClass; 31 | } 32 | 33 | @Override 34 | protected void renderBody() { 35 | sink.paragraph(); 36 | sink.link("visualdiff.html"); 37 | sink.text("< back to overview"); 38 | sink.link_(); 39 | sink.paragraph_(); 40 | 41 | ImmutableListMultimap perMethod = VisualDiffMetaGrouper 42 | .byTestMethod(diffs); 43 | VisualDiffReportUtil.renderTable(sink, Depth.METHOD, "Result for Test " + testClass, 44 | perMethod); 45 | 46 | for (String testMethod : perMethod.keySet()) { 47 | sink.section3(); 48 | sink.sectionTitle3(); 49 | sink.text("Test Method " + testMethod); 50 | sink.sectionTitle3_(); 51 | sink.section3_(); 52 | 53 | for (VisualDiffMeta diffMeta : perMethod.get(testMethod)) { 54 | sink.section4(); 55 | sink.sectionTitle4(); 56 | sink.text("Screenshot ID " + diffMeta.screenshotId); 57 | sink.sectionTitle4_(); 58 | 59 | sink.table(); 60 | sink.tableRow(); 61 | 62 | renderScreenshot("images/visualdiff/" + diffMeta.getScreenshot1Filepath(), "new"); 63 | 64 | if (diffMeta.diffType == Type.DIFFERENT) { 65 | renderScreenshot("images/visualdiff/" + diffMeta.getScreenshotDiffFilepath(), 66 | "differences"); 67 | renderScreenshot("images/visualdiff/" + diffMeta.getScreenshot2Filepath(), 68 | "old"); 69 | } 70 | 71 | sink.tableRow_(); 72 | sink.table_(); 73 | 74 | sink.section4_(); 75 | } 76 | 77 | sink.link(VisualDiffReportUtil.asFilename(diffs.get(0).testClass, ".html")); 78 | sink.text("^ top"); 79 | sink.link_(); 80 | } 81 | } 82 | 83 | private void renderScreenshot(final String path, final String type) { 84 | SinkEventAttributeSet cellAttr = new SinkEventAttributeSet(); 85 | cellAttr.addAttribute(SinkEventAttributes.ALIGN, "center"); 86 | 87 | SinkEventAttributeSet smallImagesAttr = new SinkEventAttributeSet(); 88 | smallImagesAttr.addAttribute(SinkEventAttributes.HEIGHT, "200"); 89 | smallImagesAttr.addAttribute(SinkEventAttributes.HSPACE, "20"); 90 | smallImagesAttr.addAttribute(SinkEventAttributes.VSPACE, "10"); 91 | smallImagesAttr.addAttribute(SinkEventAttributes.BORDER, "1"); 92 | smallImagesAttr.addAttribute(SinkEventAttributes.ALT, type); 93 | 94 | sink.tableCell(cellAttr); 95 | 96 | sink.link(path); 97 | sink.figureGraphics(path, smallImagesAttr); 98 | sink.link_(); 99 | 100 | sink.lineBreak(); 101 | 102 | sink.text(type); 103 | 104 | sink.tableCell_(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.kreyssel.selenium2.visualdiff 7 | visualdiff-parent 8 | 1.0.0-SNAPSHOT 9 | pom 10 | 11 | VisualDiff :: Project 12 | Project to support to create Screenshots within Funtional Selenium Tests, package this screenshots as artifact on release and compare this with previous screenshots. 13 | 14 | 15 | 2.2.1 16 | 17 | 18 | 19 | core 20 | maven-plugin 21 | it 22 | 23 | 24 | 25 | 2.2.1 26 | 2.3.1 27 | 28 | 29 | 30 | 31 | 32 | ${project.groupId} 33 | visualdiff-core 34 | ${project.version} 35 | 36 | 37 | 38 | org.seleniumhq.selenium 39 | selenium-api 40 | ${selenium.version} 41 | 42 | 43 | org.seleniumhq.selenium 44 | selenium-java 45 | ${selenium.version} 46 | 47 | 48 | 49 | org.apache.commons 50 | commons-lang3 51 | 3.0.1 52 | 53 | 54 | commons-io 55 | commons-io 56 | 2.0.1 57 | 58 | 59 | 60 | junit 61 | junit 62 | 4.8.2 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | 2.3.2 74 | 75 | 76 | 1.5 77 | 1.5 78 | 79 | 80 | 81 | 82 | org.mortbay.jetty 83 | jetty-maven-plugin 84 | 7.4.5.v20110725 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-site-plugin 90 | 3.0 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | maven-3 100 | 101 | 102 | 104 | ${basedir} 105 | 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-site-plugin 112 | 113 | 114 | attach-descriptor 115 | 116 | attach-descriptor 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | --------------------------------------------------------------------------------