diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..455d8b5 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,9 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/openjdk:8-node-browsers + steps: + - checkout + - run: npm install + - run: ./run-snapshots.sh diff --git a/package-lock.json b/package-lock.json index 99fa222..2101b59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -281,9 +281,9 @@ } }, "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", + "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -1708,9 +1708,9 @@ "dev": true }, "har-validator": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.2.tgz", - "integrity": "sha512-OFxb5MZXCUMx43X7O8LK4FKggEQx6yC5QPmOcBnYbJ9UjxEcMcrMbaR0af5HZpqeFopw2GwQRQi34ZXI7YLM5w==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { "ajv": "^6.5.5", diff --git a/package.json b/package.json index 719416a..a47f73d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "private": true, "devDependencies": { - "@percy/agent": "^0.1.0" + "@percy/agent": "^0.1.0", + "har-validator": "^5.1.3" } } diff --git a/pom.xml b/pom.xml index 8ba1a57..95e9d6b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,7 @@ - + 4.0.0 io.percy @@ -37,6 +38,7 @@ UTF-8 + 5.3.1 @@ -46,6 +48,19 @@ selenium-java 3.141.0 + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + @@ -95,7 +110,10 @@ maven-compiler-plugin 3.8.0 - 8 + + 1.8 + 1.8 @@ -171,8 +189,14 @@ + maven-surefire-plugin - 2.20.1 + 2.22.0 + + + + false + maven-jar-plugin diff --git a/run-snapshots.sh b/run-snapshots.sh new file mode 100755 index 0000000..5fce290 --- /dev/null +++ b/run-snapshots.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -o pipefail +set -e + +# If we don't have a local Percy agent binary, run npm install. +if [ ! -f ./node_modules/.bin/percy ]; then + echo "*** Percy agent not installed. Installing now. ***" + npm install +fi + +# Run the tests with snapshots. +./node_modules/.bin/percy exec -- mvn test diff --git a/src/test/java/io/percy/selenium/SdkTest.java b/src/test/java/io/percy/selenium/SdkTest.java new file mode 100644 index 0000000..9ef31b5 --- /dev/null +++ b/src/test/java/io/percy/selenium/SdkTest.java @@ -0,0 +1,85 @@ +package io.percy.selenium; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; + +public class SdkTest { + private static final String TEST_URL = "http://localhost:8000"; + private static WebDriver driver; + private static Percy percy; + + @BeforeAll + public static void openServerAndBrowser() throws IOException { + TestServer.startServer(); + ChromeOptions options = new ChromeOptions(); + options.addArguments( + "--headless", + "--disable-web-security", + "--allow-running-insecure-content", + "--ignore-certificate-errors"); + driver = new ChromeDriver(options); + percy = new Percy(driver); + } + + @AfterAll + public static void closeServerAndBrowser() { + // Close our test browser. + driver.quit(); + // Shutdown our server and make sure the threadpool also terminates. + TestServer.shutdown(); + } + + @AfterEach + public void clearLocalStorage() { + ((JavascriptExecutor) driver).executeScript("window.localStorage.clear()"); + } + + @Test + public void takesLocalAppSnapshotWithProvidedName() { + driver.get(TEST_URL); + percy.snapshot("Snapshot with provided name"); + } + + @Test + public void takesLocalAppSnapshotWithProvidedNameAndWidths() { + driver.get(TEST_URL); + percy.snapshot("Snapshot with provided name and widths", Arrays.asList(768, 992, 1200)); + } + + @Test + public void takesLocalAppSnapshotWithProvidedNameAndMinHeight() { + driver.get(TEST_URL); + percy.snapshot("Snapshot with provided name and min height", null, 2000); + } + + @Test + public void takesMultipleSnapshotsInOneTestCase() { + driver.get(TEST_URL); + + WebElement newTodoEl = driver.findElement(By.className("new-todo")); + newTodoEl.sendKeys("A new todo to check off"); + newTodoEl.sendKeys(Keys.RETURN); + percy.snapshot("Multiple snapshots in one test case -- #1", Arrays.asList(768, 992, 1200)); + + driver.findElement(By.cssSelector("input.toggle")).click(); + percy.snapshot("Multiple snapshots in one test case -- #2", Arrays.asList(768, 992, 1200)); + } + + @Test + public void snapshotsLiveHTTPSSite() { + driver.get("https://www.google.com"); + percy.snapshot("Live HTTPS site", Arrays.asList(768, 992, 1200)); + } +} diff --git a/src/test/java/io/percy/selenium/TestServer.java b/src/test/java/io/percy/selenium/TestServer.java new file mode 100644 index 0000000..b5a3636 --- /dev/null +++ b/src/test/java/io/percy/selenium/TestServer.java @@ -0,0 +1,98 @@ +package io.percy.selenium; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +/** + * HTTP server that serves the static files that make our test app. + */ +class TestServer { + // Server port. + private static final Integer PORT = 8000; + + // Location for static files in our test app. + private static final String TESTAPP_DIR = "src/test/resources/testapp/"; + private static final String INDEX_FILE = "index.html"; + + // Recognized Mime type map (extension -> mimetype) + private static final Map MIME_MAP = new HashMap(); + static { + MIME_MAP.put("html", "text/html"); + MIME_MAP.put("js", "application/javascript"); + MIME_MAP.put("css", "text/css"); + } + + private static ExecutorService executor; + private static HttpServer server; + + public static HttpServer startServer() throws IOException { + executor = Executors.newFixedThreadPool(1); + server = HttpServer.create(new InetSocketAddress(PORT), 0); + HttpContext context = server.createContext("/"); + context.setHandler(TestServer::handleRequest); + server.setExecutor(executor); + server.start(); + return server; + } + + public static void shutdown() { + if (server != null) { + server.stop(1); + } + if (executor != null) { + executor.shutdownNow(); + } + } + + private static void handleRequest(HttpExchange exchange) throws IOException { + String requestedPath = exchange.getRequestURI().getPath(); + if (requestedPath.equals("/")) { + serveStaticFile(exchange, INDEX_FILE); + } else { + if (requestedPath.startsWith("/")) { + requestedPath = requestedPath.substring(1); + } + serveStaticFile(exchange, requestedPath); + } + } + + private static void serveStaticFile(HttpExchange exchange, String resourcePath) throws IOException { + byte[] response; + int responseCode; + File file = new File(String.format("%s/%s", TESTAPP_DIR, resourcePath)); + if (!file.canRead()) { + response = "404 - File Not Found".getBytes(); + responseCode = 404; + } else { + InputStream in = new FileInputStream(file); + response = new byte[in.available()]; + in.read(response); + responseCode = 200; + in.close(); + } + + exchange.getResponseHeaders().add("Content-Type", getMimeType(resourcePath)); + exchange.sendResponseHeaders(responseCode, response.length); + OutputStream os = exchange.getResponseBody(); + os.write(response); + os.close(); + } + + private static String getMimeType(String resourcePath) { + int lastDotIndex = resourcePath.lastIndexOf('.'); + String extension = lastDotIndex > 0 ? resourcePath.substring(lastDotIndex + 1) : ""; + return MIME_MAP.getOrDefault(extension, "text/plain"); + } +} diff --git a/src/test/resources/testapp/README.md b/src/test/resources/testapp/README.md new file mode 100644 index 0000000..4e4badd --- /dev/null +++ b/src/test/resources/testapp/README.md @@ -0,0 +1,11 @@ +App for testing the SDK. Based on the +[TodoMVC](https://github.com/tastejs/todomvc) +[Vanilla-ES6](https://github.com/tastejs/todomvc/tree/master/examples/vanilla-es6) +app, forked at commit +[c78ae12a1834a11da6236c64a0c0fb06b20b7c51](https://github.com/tastejs/todomvc/tree/c78ae12a1834a11da6236c64a0c0fb06b20b7c51). + +To see the app in action: + +```bash +$ open index.html +``` diff --git a/src/test/resources/testapp/base.css b/src/test/resources/testapp/base.css new file mode 100644 index 0000000..da65968 --- /dev/null +++ b/src/test/resources/testapp/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/src/test/resources/testapp/base.js b/src/test/resources/testapp/base.js new file mode 100644 index 0000000..a56b5aa --- /dev/null +++ b/src/test/resources/testapp/base.js @@ -0,0 +1,249 @@ +/* global _ */ +(function () { + 'use strict'; + + /* jshint ignore:start */ + // Underscore's Template Module + // Courtesy of underscorejs.org + var _ = (function (_) { + _.defaults = function (object) { + if (!object) { + return object; + } + for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { + var iterable = arguments[argsIndex]; + if (iterable) { + for (var key in iterable) { + if (object[key] == null) { + object[key] = iterable[key]; + } + } + } + } + return object; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + return _; + })({}); + + if (location.hostname === 'todomvc.com') { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + ga('create', 'UA-31081062-1', 'auto'); + ga('send', 'pageview'); + } + /* jshint ignore:end */ + + function redirect() { + if (location.hostname === 'tastejs.github.io') { + location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com'); + } + } + + function findRoot() { + var base = location.href.indexOf('examples/'); + return location.href.substr(0, base); + } + + function getFile(file, callback) { + if (!location.host) { + return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.'); + } + + var xhr = new XMLHttpRequest(); + + xhr.open('GET', findRoot() + file, true); + xhr.send(); + + xhr.onload = function () { + if (xhr.status === 200 && callback) { + callback(xhr.responseText); + } + }; + } + + function Learn(learnJSON, config) { + if (!(this instanceof Learn)) { + return new Learn(learnJSON, config); + } + + var template, framework; + + if (typeof learnJSON !== 'object') { + try { + learnJSON = JSON.parse(learnJSON); + } catch (e) { + return; + } + } + + if (config) { + template = config.template; + framework = config.framework; + } + + if (!template && learnJSON.templates) { + template = learnJSON.templates.todomvc; + } + + if (!framework && document.querySelector('[data-framework]')) { + framework = document.querySelector('[data-framework]').dataset.framework; + } + + this.template = template; + + if (learnJSON.backend) { + this.frameworkJSON = learnJSON.backend; + this.frameworkJSON.issueLabel = framework; + this.append({ + backend: true + }); + } else if (learnJSON[framework]) { + this.frameworkJSON = learnJSON[framework]; + this.frameworkJSON.issueLabel = framework; + this.append(); + } + + this.fetchIssueCount(); + } + + Learn.prototype.append = function (opts) { + var aside = document.createElement('aside'); + aside.innerHTML = _.template(this.template, this.frameworkJSON); + aside.className = 'learn'; + + if (opts && opts.backend) { + // Remove demo link + var sourceLinks = aside.querySelector('.source-links'); + var heading = sourceLinks.firstElementChild; + var sourceLink = sourceLinks.lastElementChild; + // Correct link path + var href = sourceLink.getAttribute('href'); + sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http'))); + sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML; + } else { + // Localize demo links + var demoLinks = aside.querySelectorAll('.demo-link'); + Array.prototype.forEach.call(demoLinks, function (demoLink) { + if (demoLink.getAttribute('href').substr(0, 4) !== 'http') { + demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href')); + } + }); + } + + document.body.className = (document.body.className + ' learn-bar').trim(); + document.body.insertAdjacentHTML('afterBegin', aside.outerHTML); + }; + + Learn.prototype.fetchIssueCount = function () { + var issueLink = document.getElementById('issue-count-link'); + if (issueLink) { + var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos'); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onload = function (e) { + var parsedResponse = JSON.parse(e.target.responseText); + if (parsedResponse instanceof Array) { + var count = parsedResponse.length; + if (count !== 0) { + issueLink.innerHTML = 'This app has ' + count + ' open issues'; + document.getElementById('issue-count').style.display = 'inline'; + } + } + }; + xhr.send(); + } + }; + + redirect(); + getFile('learn.json', Learn); +})(); diff --git a/src/test/resources/testapp/bundle.js b/src/test/resources/testapp/bundle.js new file mode 100644 index 0000000..37a8ebb --- /dev/null +++ b/src/test/resources/testapp/bundle.js @@ -0,0 +1,57 @@ +var $jscomp={scope:{},getGlobal:function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global?global:a}};$jscomp.global=$jscomp.getGlobal(this);$jscomp.initSymbol=function(){$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol);$jscomp.initSymbol=function(){}};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(a){return"jscomp_symbol_"+a+$jscomp.symbolCounter_++}; +$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();$jscomp.global.Symbol.iterator||($jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));$jscomp.initSymbolIterator=function(){}};$jscomp.makeIterator=function(a){$jscomp.initSymbolIterator();if(a[$jscomp.global.Symbol.iterator])return a[$jscomp.global.Symbol.iterator]();var b=0;return{next:function(){return b==a.length?{done:!0}:{done:!1,value:a[b++]}}}}; +$jscomp.arrayFromIterator=function(a){for(var b,c=[];!(b=a.next()).done;)c.push(b.value);return c};$jscomp.arrayFromIterable=function(a){return a instanceof Array?a:$jscomp.arrayFromIterator($jscomp.makeIterator(a))}; +$jscomp.inherits=function(a,b){function c(){}c.prototype=b.prototype;a.prototype=new c;a.prototype.constructor=a;for(var d in b)if($jscomp.global.Object.defineProperties){var e=$jscomp.global.Object.getOwnPropertyDescriptor(b,d);e&&$jscomp.global.Object.defineProperty(a,d,e)}else a[d]=b[d]};$jscomp.array=$jscomp.array||{};$jscomp.array.done_=function(){return{done:!0,value:void 0}}; +$jscomp.array.arrayIterator_=function(a,b){a instanceof String&&(a=String(a));var c=0;$jscomp.initSymbol();$jscomp.initSymbolIterator();var d={},e=(d.next=function(){if(cb;)--c in this?this[--a]=this[c]:delete this[a];return this};$jscomp.array.copyWithin$install=function(){Array.prototype.copyWithin||(Array.prototype.copyWithin=$jscomp.array.copyWithin)}; +$jscomp.array.fill=function(a,b,c){null!=c&&a.length||(c=this.length||0);c=Number(c);for(b=Number((void 0===b?0:b)||0);b>>0;if(0===a)return 32;var b=0;0===(a&4294901760)&&(a<<=16,b+=16);0===(a&4278190080)&&(a<<=8,b+=8);0===(a&4026531840)&&(a<<=4,b+=4);0===(a&3221225472)&&(a<<=2,b+=2);0===(a&2147483648)&&b++;return b};$jscomp.math.imul=function(a,b){a=Number(a);b=Number(b);var c=a&65535,d=b&65535;return c*d+((a>>>16&65535)*d+c*(b>>>16&65535)<<16>>>0)|0};$jscomp.math.sign=function(a){a=Number(a);return 0===a||isNaN(a)?a:0a&&-.25a&&-.25a?-b:b};$jscomp.math.acosh=function(a){a=Number(a);return Math.log(a+Math.sqrt(a*a-1))};$jscomp.math.asinh=function(a){a=Number(a);if(0===a)return a;var b=Math.log(Math.abs(a)+Math.sqrt(a*a+1));return 0>a?-b:b}; +$jscomp.math.atanh=function(a){a=Number(a);return($jscomp.math.log1p(a)-$jscomp.math.log1p(-a))/2}; +$jscomp.math.hypot=function(a,b,c){for(var d=[],e=2;ef){a/=f;b/=f;g=a*a+b*b;d=$jscomp.makeIterator(d);for(e=d.next();!e.done;e=d.next())e=e.value,e=Number(e)/f,g+=e*e;return Math.sqrt(g)*f}f=a*a+b*b;d=$jscomp.makeIterator(d);for(e=d.next();!e.done;e=d.next())e=e.value,e=Number(e),f+= +e*e;return Math.sqrt(f)};$jscomp.math.trunc=function(a){a=Number(a);if(isNaN(a)||Infinity===a||-Infinity===a||0===a)return a;var b=Math.floor(Math.abs(a));return 0>a?-b:b};$jscomp.math.cbrt=function(a){if(0===a)return a;a=Number(a);var b=Math.pow(Math.abs(a),1/3);return 0>a?-b:b};$jscomp.number=$jscomp.number||{};$jscomp.number.isFinite=function(a){return"number"!==typeof a?!1:!isNaN(a)&&Infinity!==a&&-Infinity!==a}; +$jscomp.number.isInteger=function(a){return $jscomp.number.isFinite(a)?a===Math.floor(a):!1};$jscomp.number.isNaN=function(a){return"number"===typeof a&&isNaN(a)};$jscomp.number.isSafeInteger=function(a){return $jscomp.number.isInteger(a)&&Math.abs(a)<=$jscomp.number.MAX_SAFE_INTEGER};$jscomp.number.EPSILON=Math.pow(2,-52);$jscomp.number.MAX_SAFE_INTEGER=9007199254740991;$jscomp.number.MIN_SAFE_INTEGER=-9007199254740991;$jscomp.object=$jscomp.object||{}; +$jscomp.object.assign=function(a,b){for(var c=[],d=1;dd||1114111=d?c+=String.fromCharCode(d):(d-=65536,c+=String.fromCharCode(d>>>10&1023|55296),c+=String.fromCharCode(d&1023|56320))}return c}; +$jscomp.string.repeat=function(a){var b=this.toString();if(0>a||1342177279>>=1)b+=b;return c};$jscomp.string.repeat$install=function(){String.prototype.repeat||(String.prototype.repeat=$jscomp.string.repeat)}; +$jscomp.string.codePointAt=function(a){var b=this.toString(),c=b.length;a=Number(a)||0;if(0<=a&&ad||56319a||57343=e};$jscomp.string.startsWith$install=function(){String.prototype.startsWith||(String.prototype.startsWith=$jscomp.string.startsWith)}; +$jscomp.string.endsWith=function(a,b){$jscomp.string.noRegExp_(a,"endsWith");var c=this.toString();a+="";void 0===b&&(b=c.length);for(var d=Math.max(0,Math.min(b|0,c.length)),e=a.length;0=e};$jscomp.string.endsWith$install=function(){String.prototype.endsWith||(String.prototype.endsWith=$jscomp.string.endsWith)}; +var module$src$item={},Item$$module$src$item,ItemList$$module$src$item,Empty$$module$src$item={Record:{}},EmptyItemQuery$$module$src$item,emptyItemQuery$$module$src$item=Empty$$module$src$item.Record,ItemQuery$$module$src$item,ItemUpdate$$module$src$item;module$src$item.emptyItemQuery=emptyItemQuery$$module$src$item;var module$src$store={},Store$$module$src$store=function(a,b){var c=window.localStorage,d;this.getLocalStorage=function(){return d||JSON.parse(c.getItem(a)||"[]")};this.setLocalStorage=function(b){c.setItem(a,JSON.stringify(d=b))};b&&b()};Store$$module$src$store.prototype.find=function(a,b){var c=this.getLocalStorage(),d;b(c.filter(function(b){for(d in a)if(a[d]!==b[d])return!1;return!0}))}; +Store$$module$src$store.prototype.update=function(a,b){for(var c=a.id,d=this.getLocalStorage(),e=d.length,f;e--;)if(d[e].id===c){for(f in a)d[e][f]=a[f];break}this.setLocalStorage(d);b&&b()};Store$$module$src$store.prototype.insert=function(a,b){var c=this.getLocalStorage();c.push(a);this.setLocalStorage(c);b&&b()};Store$$module$src$store.prototype.remove=function(a,b){var c,d=this.getLocalStorage().filter(function(b){for(c in a)if(a[c]!==b[c])return!0;return!1});this.setLocalStorage(d);b&&b(d)}; +Store$$module$src$store.prototype.count=function(a){this.find(module$src$item.emptyItemQuery,function(b){for(var c=b.length,d=c,e=0;d--;)e+=b[d].completed;a(c,c-e,e)})};module$src$store["default"]=Store$$module$src$store;var module$src$helpers={};function qs$$module$src$helpers(a,b){return(b||document).querySelector(a)}function $on$$module$src$helpers(a,b,c,d){a.addEventListener(b,c,!!d)}function $delegate$$module$src$helpers(a,b,c,d,e){$on$$module$src$helpers(a,c,function(c){for(var e=c.target,h=a.querySelectorAll(b),k=h.length;k--;)if(h[k]===e){d.call(e,c);break}},!!e)}var escapeForHTML$$module$src$helpers=function(a){return a.replace(/[&<]/g,function(a){return"&"===a?"&":"<"})};module$src$helpers.qs=qs$$module$src$helpers; +module$src$helpers.$on=$on$$module$src$helpers;module$src$helpers.$delegate=$delegate$$module$src$helpers;module$src$helpers.escapeForHTML=escapeForHTML$$module$src$helpers;var module$src$template={},Template$$module$src$template=function(){};Template$$module$src$template.prototype.itemList=function(a){return a.reduce(function(a,c){return a+('\n
  • \n\t
    \n\t\t\n\t\t\n\t\t\n\t
    \n
  • ')},"")}; +Template$$module$src$template.prototype.itemCounter=function(a){return a+" item"+(1!==a?"s":"")+" left"};module$src$template["default"]=Template$$module$src$template;var module$src$view={},_itemId$$module$src$view=function(a){return parseInt(a.parentNode.dataset.id||a.parentNode.parentNode.dataset.id,10)},ENTER_KEY$$module$src$view=13,ESCAPE_KEY$$module$src$view=27,View$$module$src$view=function(a){var b=this;this.template=a;this.$todoList=module$src$helpers.qs(".todo-list");this.$todoItemCounter=module$src$helpers.qs(".todo-count");this.$clearCompleted=module$src$helpers.qs(".clear-completed");this.$main=module$src$helpers.qs(".main");this.$toggleAll=module$src$helpers.qs(".toggle-all"); +this.$newTodo=module$src$helpers.qs(".new-todo");module$src$helpers.$delegate(this.$todoList,"li label","dblclick",function(a){b.editItem(a.target)})};View$$module$src$view.prototype.editItem=function(a){var b=a.parentElement.parentElement;b.classList.add("editing");var c=document.createElement("input");c.className="edit";c.value=a.innerText;b.appendChild(c);c.focus()};View$$module$src$view.prototype.showItems=function(a){this.$todoList.innerHTML=this.template.itemList(a)}; +View$$module$src$view.prototype.removeItem=function(a){(a=module$src$helpers.qs('[data-id="'+a+'"]'))&&this.$todoList.removeChild(a)};View$$module$src$view.prototype.setItemsLeft=function(a){this.$todoItemCounter.innerHTML=this.template.itemCounter(a)};View$$module$src$view.prototype.setClearCompletedButtonVisibility=function(a){this.$clearCompleted.style.display=a?"block":"none"};View$$module$src$view.prototype.setMainVisibility=function(a){this.$main.style.display=a?"block":"none"}; +View$$module$src$view.prototype.setCompleteAllCheckbox=function(a){this.$toggleAll.checked=!!a};View$$module$src$view.prototype.updateFilterButtons=function(a){module$src$helpers.qs(".filters .selected").className="";module$src$helpers.qs('.filters [href="#/'+a+'"]').className="selected"};View$$module$src$view.prototype.clearNewTodo=function(){this.$newTodo.value=""}; +View$$module$src$view.prototype.setItemComplete=function(a,b){var c=module$src$helpers.qs('[data-id="'+a+'"]');c&&(c.className=b?"completed":"",module$src$helpers.qs("input",c).checked=b)};View$$module$src$view.prototype.editItemDone=function(a,b){var c=module$src$helpers.qs('[data-id="'+a+'"]'),d=module$src$helpers.qs("input.edit",c);c.removeChild(d);c.classList.remove("editing");module$src$helpers.qs("label",c).textContent=b}; +View$$module$src$view.prototype.bindAddItem=function(a){module$src$helpers.$on(this.$newTodo,"change",function(b){(b=b.target.value.trim())&&a(b)})};View$$module$src$view.prototype.bindRemoveCompleted=function(a){module$src$helpers.$on(this.$clearCompleted,"click",a)};View$$module$src$view.prototype.bindToggleAll=function(a){module$src$helpers.$on(this.$toggleAll,"click",function(b){a(b.target.checked)})}; +View$$module$src$view.prototype.bindRemoveItem=function(a){module$src$helpers.$delegate(this.$todoList,".destroy","click",function(b){a(_itemId$$module$src$view(b.target))})};View$$module$src$view.prototype.bindToggleItem=function(a){module$src$helpers.$delegate(this.$todoList,".toggle","click",function(b){b=b.target;a(_itemId$$module$src$view(b),b.checked)})}; +View$$module$src$view.prototype.bindEditItemSave=function(a){module$src$helpers.$delegate(this.$todoList,"li .edit","blur",function(b){b=b.target;b.dataset.iscanceled||a(_itemId$$module$src$view(b),b.value.trim())},!0);module$src$helpers.$delegate(this.$todoList,"li .edit","keypress",function(a){var c=a.target;a.keyCode===ENTER_KEY$$module$src$view&&c.blur()})}; +View$$module$src$view.prototype.bindEditItemCancel=function(a){module$src$helpers.$delegate(this.$todoList,"li .edit","keyup",function(b){var c=b.target;b.keyCode===ESCAPE_KEY$$module$src$view&&(c.dataset.iscanceled=!0,c.blur(),a(_itemId$$module$src$view(c)))})};module$src$view["default"]=View$$module$src$view;var module$src$controller={},Controller$$module$src$controller=function(a,b){var c=this;this.store=a;this.view=b;b.bindAddItem(this.addItem.bind(this));b.bindEditItemSave(this.editItemSave.bind(this));b.bindEditItemCancel(this.editItemCancel.bind(this));b.bindRemoveItem(this.removeItem.bind(this));b.bindToggleItem(function(a,b){c.toggleCompleted(a,b);c._filter()});b.bindRemoveCompleted(this.removeCompletedItems.bind(this));b.bindToggleAll(this.toggleAll.bind(this));this._activeRoute="";this._lastActiveRoute= +null};Controller$$module$src$controller.prototype.setView=function(a){this._activeRoute=a=a.replace(/^#\//,"");this._filter();this.view.updateFilterButtons(a)};Controller$$module$src$controller.prototype.addItem=function(a){var b=this;this.store.insert({id:Date.now(),title:a,completed:!1},function(){b.view.clearNewTodo();b._filter(!0)})}; +Controller$$module$src$controller.prototype.editItemSave=function(a,b){var c=this;b.length?this.store.update({id:a,title:b},function(){c.view.editItemDone(a,b)}):this.removeItem(a)};Controller$$module$src$controller.prototype.editItemCancel=function(a){var b=this;this.store.find({id:a},function(c){b.view.editItemDone(a,c[0].title)})};Controller$$module$src$controller.prototype.removeItem=function(a){var b=this;this.store.remove({id:a},function(){b._filter();b.view.removeItem(a)})}; +Controller$$module$src$controller.prototype.removeCompletedItems=function(){this.store.remove({completed:!0},this._filter.bind(this))};Controller$$module$src$controller.prototype.toggleCompleted=function(a,b){var c=this;this.store.update({id:a,completed:b},function(){c.view.setItemComplete(a,b)})}; +Controller$$module$src$controller.prototype.toggleAll=function(a){var b=this;this.store.find({completed:!a},function(c){c=$jscomp.makeIterator(c);for(var d=c.next();!d.done;d=c.next())b.toggleCompleted(d.value.id,a)});this._filter()}; +Controller$$module$src$controller.prototype._filter=function(a){var b=this,c=this._activeRoute;(a||""!==this._lastActiveRoute||this._lastActiveRoute!==c)&&this.store.find({"":module$src$item.emptyItemQuery,active:{completed:!1},completed:{completed:!0}}[c],this.view.showItems.bind(this.view));this.store.count(function(a,c,f){b.view.setItemsLeft(c);b.view.setClearCompletedButtonVisibility(f);b.view.setCompleteAllCheckbox(f===a);b.view.setMainVisibility(a)});this._lastActiveRoute=c}; +module$src$controller["default"]=Controller$$module$src$controller;var store$$module$src$app=new module$src$store["default"]("todos-vanilla-es6"),template$$module$src$app=new module$src$template["default"],view$$module$src$app=new module$src$view["default"](template$$module$src$app),controller$$module$src$app=new module$src$controller["default"](store$$module$src$app,view$$module$src$app),setView$$module$src$app=function(){return controller$$module$src$app.setView(document.location.hash)};module$src$helpers.$on(window,"load",setView$$module$src$app); +module$src$helpers.$on(window,"hashchange",setView$$module$src$app); diff --git a/src/test/resources/testapp/index.css b/src/test/resources/testapp/index.css new file mode 100644 index 0000000..3ac79f0 --- /dev/null +++ b/src/test/resources/testapp/index.css @@ -0,0 +1,379 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +:focus { + outline: 0; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + width: 60px; + height: 34px; + font-size: 0; + position: absolute; + top: -52px; + left: -13px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all + label:before { + content: '❯'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked + label:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: '×'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + font-size: 10px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} diff --git a/src/test/resources/testapp/index.html b/src/test/resources/testapp/index.html new file mode 100644 index 0000000..5becede --- /dev/null +++ b/src/test/resources/testapp/index.html @@ -0,0 +1,45 @@ + + + + + Vanilla ES6 • TodoMVC + + + +
    +
    +

    todos

    + +
    + +
    + + + + + +