├── .gitignore ├── .istanbul.yml ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── app.js ├── ideas-for-tips.md ├── lib ├── checker.js ├── checks │ ├── compatibility │ │ ├── css-prefixes.js │ │ └── flash-detection.js │ ├── integration │ │ ├── manifest.js │ │ └── web-manifest.json │ ├── interactions │ │ ├── alert.js │ │ └── touchscreen-target.js │ ├── performance │ │ ├── compression.js │ │ ├── exif.js │ │ ├── http-errors.js │ │ ├── number-requests.js │ │ └── redirects.js │ ├── responsive │ │ ├── doc-width.js │ │ ├── fonts-size.js │ │ ├── meta-viewport.js │ │ └── screenshot.js │ └── utils.js ├── css-prefixes.json ├── ejs-filters.js ├── issues-scale.json ├── logs │ └── logs.ejs ├── profiles │ ├── default.json │ ├── smartphone2.json │ └── tablet.json ├── reports │ ├── compatibility │ │ ├── css-prefixes │ │ │ ├── mismatching-prefixes.ejs │ │ │ └── missing-prefixes.ejs │ │ └── flash-detection │ │ │ ├── swf-file-detected.ejs │ │ │ └── swfobject-lib-detected.ejs │ ├── format.js │ ├── integration │ │ └── manifest │ │ │ ├── httperror.ejs │ │ │ ├── httperror_property.ejs │ │ │ ├── jsonerror.ejs │ │ │ ├── jsonserror.ejs │ │ │ ├── multiple-manifests.ejs │ │ │ └── networkerror_property.ejs │ ├── interactions │ │ ├── alert │ │ │ └── alert-detected.ejs │ │ └── touchscreen-target │ │ │ └── too-small-touchscreen-target.ejs │ ├── performance │ │ ├── compression │ │ │ └── resources-could-be-compressed.ejs │ │ ├── exif │ │ │ └── images-could-be-unexified.ejs │ │ ├── http-errors │ │ │ ├── favicon.ejs │ │ │ └── http-errors-detected.ejs │ │ ├── number-requests │ │ │ └── info-number-requests.ejs │ │ └── redirects │ │ │ └── redirects-encountered.ejs │ └── responsive │ │ ├── doc-width │ │ └── doc-width-too-large.ejs │ │ ├── fonts-size │ │ └── too-small-font-size.ejs │ │ └── meta-viewport │ │ ├── content-viewport-missed.ejs │ │ ├── hardcoded-viewport-width.ejs │ │ ├── invalid-viewport-value.ejs │ │ ├── no-viewport-declared.ejs │ │ ├── non-standard-viewport-parameter-declared.ejs │ │ ├── several-viewports-declared.ejs │ │ ├── unknow-viewport-parameter-declared.ejs │ │ └── users-are-prevented-to-zoom.ejs └── tips │ ├── accessible.html │ ├── appmanifest.html │ ├── deviceapis.html │ ├── dontpushaway.html │ ├── offline.html │ ├── pushnotifications.html │ ├── responsiveimages.html │ └── serviceworker.html ├── package.json ├── public ├── css │ ├── bootstrap-sortable.css │ ├── bootstrap-theme.min.css │ ├── bootstrap.min.css │ ├── logs.css │ ├── report.css │ └── style.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── img │ ├── cog.svg │ ├── developers-min.svg │ ├── favicon.ico │ ├── heart.svg │ ├── mobile-checker-icon.png │ ├── mobile-checker-icon.svg │ ├── mobile-checker.svg │ ├── mobilechecker-logo-w3c.png │ ├── mobilechecker-logo-w3c.svg │ ├── mobilechecker-logo.png │ ├── opensource.svg │ ├── opensource_logo.png │ ├── smartphone.svg │ ├── smartphone2.svg │ ├── tablet.svg │ ├── w3c-developers-test-3.svg │ ├── w3c-developers.svg │ └── w3c.svg ├── index.html ├── js │ ├── bootstrap-sortable.js │ ├── bootstrap.min.js │ ├── jquery.min.js │ ├── logs.js │ ├── script.js │ └── socket.io.js ├── logs.ejs ├── manifest.json └── octicons │ ├── LICENSE.txt │ ├── README.md │ ├── octicons-local.ttf │ ├── octicons.css │ ├── octicons.eot │ ├── octicons.less │ ├── octicons.svg │ ├── octicons.ttf │ ├── octicons.woff │ └── sprockets-octicons.scss ├── test ├── mocha.opts ├── test_server │ ├── public │ │ ├── css │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap.min.css │ │ │ └── style.css │ │ ├── docs │ │ │ ├── 404-manifest.html │ │ │ ├── 404-manifest.json │ │ │ ├── badjson-manifest.html │ │ │ ├── badmanifest.json │ │ │ ├── brokenlinks-manifest.html │ │ │ ├── brokenlinks-manifest.json │ │ │ ├── brokenlinks-manifest2.html │ │ │ ├── brokenlinks-manifest2.json │ │ │ ├── compressed.html │ │ │ ├── css-inconsistent-prefixes.html │ │ │ ├── css-prefixes.html │ │ │ ├── firework.jpg │ │ │ ├── font-size-ok.html │ │ │ ├── http-errors-favicon.html │ │ │ ├── http-errors.html │ │ │ ├── icon1.png │ │ │ ├── icon2.svg │ │ │ ├── inconsistent-prefixes.css │ │ │ ├── invalid-manifest.html │ │ │ ├── invalidmanifest.json │ │ │ ├── manifest.json │ │ │ ├── multiple-manifests.html │ │ │ ├── no-manifest.html │ │ │ ├── number-requests.html │ │ │ ├── prefixes.css │ │ │ ├── redirects.html │ │ │ ├── too-small-font-size.html │ │ │ ├── too-small-touchscreen-target.html │ │ │ ├── touchscreen-target-ok.html │ │ │ ├── uncompressed.html │ │ │ ├── viewport_incorrect-initial-scale.html │ │ │ ├── viewport_incorrect-width.html │ │ │ ├── viewport_many-viewport.html │ │ │ ├── viewport_no-initial-scale.html │ │ │ ├── viewport_no-meta-viewport.html │ │ │ ├── viewport_no-width.html │ │ │ ├── viewport_ok.html │ │ │ ├── width_fail.html │ │ │ └── width_success.html │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ └── glyphicons-halflings-regular.woff │ │ ├── index.html │ │ └── js │ │ │ ├── bootstrap.min.js │ │ │ ├── jquery.min.js │ │ │ └── script.js │ └── test_app.js └── tests.js └── tools └── getListOfCssPrefixEquivalents.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Coverage directory used by tools like istanbul 6 | coverage 7 | 8 | public/*.png 9 | node_modules 10 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | include-all-sources: true 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: node_js 3 | node_js: 4 | - "4" 5 | addons: 6 | apt: 7 | sources: 8 | - google-chrome 9 | packages: 10 | - google-chrome-stable 11 | install: 12 | - echo "Starting install phase..." 13 | - wget https://github.com/lightbody/browsermob-proxy/releases/download/browsermob-proxy-2.1.4/browsermob-proxy-2.1.4-bin.zip 14 | - unzip browsermob-proxy-2.1.4-bin.zip 15 | - ln -s browsermob-proxy-2.1.4 browsermob-proxy-2.0-beta-9 16 | - browsermob-proxy-2.1.4/bin/browsermob-proxy --use-littleproxy false & 17 | - npm install 18 | - echo "Install phase completed." 19 | - google-chrome --version 20 | before_script: 21 | - export DISPLAY=:99.0 22 | - sh -e /etc/init.d/xvfb start 23 | script: 24 | - echo "Running tests..." 25 | - mocha --timeout 20000 --globals Intl,IntlPolyfill,ErrorHandler test/tests.js 26 | - echo "Tests completed." 27 | after_script: 28 | - echo "Running coveralls..." 29 | - npm run coveralls 30 | - echo "Running coveralls completed." 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Mobile Checker 2 | 3 | Looking to contribute to Mobile Checker? There is many ways to get involved in its developement. 4 | Please take few minutes to read this document to understand how you can help in Mobile Checker developement. 5 | 6 | ## Add a new check 7 | 8 | It is pretty simple to add a new check to the mobile checker. Create a new check require only an experience in a client side JavaScript developement. 9 | 10 | A new check is composed by 2 composants: 11 | * the script itself, who will be executed on the tested web page. 12 | * the issue template, sent to the user. 13 | * testing. 14 | 15 | ### script 16 | All check scripts are located in the [lib/checks](https://github.com/w3c/Mobile-Checker/tree/master/lib/checks) directory. You just have to add your own script file in the correct category. Of course, feel free to create a new category if no category match with your check. Ready? Let's create your first check! 17 | 18 | This is the basic template of check file. Save it in the lib directory: 19 | 20 | ````javascript 21 | var self = this; 22 | exports.name = "name-of-the-check"; //write here the name of your check. Have to match with the file's name. 23 | exports.category = "name-of-the-category"; //write here the name. Have to match with a category's directory name. 24 | exports.check = function(checker, browser) { 25 | /* 26 | * here your code. 27 | */ 28 | }; 29 | ``` 30 | 31 | The ```check(checker, browser)``` function, receive ```checker``` and ```browser``` as parameters. 32 | * Use ```browser``` to test the web page. 33 | * Use ```checker``` to report the result. 34 | 35 | N.B: The ```browser``` object is based on the [mobile-web-browser-emulator API](https://github.com/w3c/mobile-web-browser-emulator) which is based on [selenium WebDriverJS API](https://code.google.com/p/selenium/wiki/WebDriverJs). That allow you to use all the functions provided by these APIs. 36 | 37 | 38 | #### access to mobile-web-browser-emulator API: 39 | ````javascript 40 | exports.check = function(checker, browser) { 41 | // get a browser object which can be use with mobile-web-browser-emulator API 42 | }; 43 | ``` 44 | example: 45 | ````javascript 46 | exports.check = function(checker, browser) { 47 | browser.takeScreenshot(__dirname + "/../screenshot.png"); //mobile-web-browser-emulator method 48 | }; 49 | ``` 50 | 51 | #### access to selenium WebDriverJS API: 52 | ````javascript 53 | browser.do(function(driver) { //take a function in parameter 54 | // get a driver object which can be use with selenium webdriver 55 | }); 56 | ``` 57 | example: 58 | ````javascript 59 | exports.check = function(checker, browser) { 60 | browser.do(function(driver) { 61 | driver.getTitle().then(function(title) { //selenium webdriver method 62 | console.log(title); 63 | }); 64 | }); 65 | }; 66 | ``` 67 | N.B: WebDriverJS uses "promises" to track the state of each operation. [learn more](https://code.google.com/p/selenium/wiki/WebDriverJs#Promises). 68 | 69 | #### run your own script: 70 | ````javascript 71 | exports.check = function(checker, browser) { 72 | browser.do(function(driver) { 73 | return driver.executeScript(function() { 74 | //write your own script and return what you need. 75 | }); 76 | }); 77 | }; 78 | ``` 79 | 80 | #### report a result: 81 | to report a result, use the report function of the checker object. 82 | ````javascript 83 | checker.report(key, name, category, status, data) 84 | ``` 85 | * **key**: name of the file issue to display in the report 86 | * **name**: self.name 87 | * **category**: self.category 88 | * **status**: error, warning or info 89 | * **data**: data to display in the issue reported (optional) 90 | 91 | 92 | ### issue template 93 | All check issues are located in the [lib/issues](https://github.com/w3c/Mobile-Checker/tree/master/lib/issues) directory. You just have to add your own issue file in the correct path as ```category/name-of-your-check/your-file```. You can create all issues you need. That allow you to use more than once the ```checker.report(key, name, category, status, data)``` function in your script. 94 | 95 | The Mobile Checker use [EJS](http://www.embeddedjs.com/) to write the issues sent to the client side. That allow you to insert data in the reported issue. 96 | 97 | template: 98 | ````html 99 |
100 |
101 | 106 |

107 |

Fix it:

108 |
109 |
110 | ``` 111 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 World Wide Web Consortium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt mobile checker by the World Wide Web Consortium](https://github.com/w3c/Mobile-Checker/blob/master/public/img/mobilechecker-logo-w3c.png) 2 | 3 | [![Build Status](https://travis-ci.org/w3c/Mobile-Checker.svg?branch=master)](https://travis-ci.org/w3c/Mobile-Checker) 4 | [![Coverage Status](https://coveralls.io/repos/w3c/Mobile-Checker/badge.svg)](https://coveralls.io/r/w3c/Mobile-Checker) 5 | [![Dependency Status](https://david-dm.org/w3c/Mobile-Checker.svg)](https://david-dm.org/w3c/Mobile-Checker) 6 | [![devDependency Status](https://david-dm.org/w3c/Mobile-Checker/dev-status.svg)](https://david-dm.org/w3c/Mobile-Checker#info=devDependencies) 7 | [![Join the chat at https://gitter.im/w3c/Mobile-Checker](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/w3c/Mobile-Checker) 8 | 9 | The Mobile Checker is a tool for Web developers who want to make their Web page or Web app work better on mobile devices. 10 | 11 | The Mobile Checker was built to provide all of us web developers with a new and helpful experience of mobile Web developement. 12 | We built the base. Now join us, and make it grant your wishes. We hope you will make it awesome. 13 | 14 | ## How does it work? 15 | This tool is a full JavaScript Web application built with [Node.js](http://nodejs.org/) and [Selenium WebDriver](http://docs.seleniumhq.org/projects/webdriver/). Based on the [mobile Web browser emulator API](https://github.com/w3c/mobile-web-browser-emulator), the Mobile Checker combines powerful technologies to simulate a Web browser on a mobile device. 16 | That's why, contrary to most of the current online mobile emulators, the Mobile Checker can provide an emulation close to what your Web app looks like on different kinds of mobile devices, including tablets and smartphones. 17 | 18 | 19 | ## Installation 20 | Mobile Checker is a Node application. It will eventually be distributed through npm, but in the meantime 21 | you can simply clone this repository: 22 | 23 | git clone https://github.com/w3c/Mobile-Checker.git 24 | 25 | 1. Install [Node.js](http://nodejs.org/) 26 | 27 | 2. Install npm dependencies: 28 | 29 | ``` 30 | npm install -d 31 | ``` 32 | 33 | 3. In addition to the npm dependencies, install: 34 | 35 | * [Google Chrome](https://www.google.com/chrome/) 36 | * [browsermob-proxy](https://github.com/lightbody/browsermob-proxy/) running on port 8080 37 | * [ImageMagick](http://www.imagemagick.org/) 38 | * [XVFB](http://www.x.org/archive/X11R7.6/doc/man/man1/Xvfb.1.xhtml) 39 | 40 | ## Running 41 | In your terminal, run: 42 | 43 | node app.js 44 | 45 | Then, connect on the localhost:3000 port. 46 | 47 | ## Testing 48 | Testing is done using mocha. Simply run: 49 | 50 | mocha --timeout 30000 51 | 52 | from the root and you will be running the test suite. Mocha can be installed with: 53 | 54 | npm install -g mocha 55 | 56 | ## Feedback and contributions 57 | 58 | * **Send feedback** about the tool and join us on the [mailing list](public-qa-dev@w3.org). 59 | * **Discuss** about the Mobile Checker in our [Gitter room](https://gitter.im/w3c/Mobile-Checker). 60 | * **Report a bug** and open an issue on [Github](https://github.com/w3c/Mobile-Checker/issues). 61 | * **Send a feature request** on [Github](https://github.com/w3c/Mobile-Checker/issues). 62 | * **Contribute** to the Mobile Checker by first reading the [contribution guidelines](https://github.com/w3c/Mobile-Checker/blob/master/CONTRIBUTING.md). 63 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | global.rootRequire = function(name) { 2 | return require(__dirname + '/' + name); 3 | }; 4 | 5 | 6 | var express = require("express"), 7 | app = express(), 8 | http = require('http').Server(app), 9 | compress = require('compression'), 10 | io = require('socket.io')(http), 11 | util = require("util"), 12 | Checker = require("./lib/checker").Checker, 13 | events = require("events"), 14 | uuid = require('node-uuid'), 15 | mkdirp = require('mkdirp'), 16 | ejs = require('ejs'), 17 | path = require('path'), 18 | fs = require("fs"), 19 | insafe = require("insafe"), 20 | formatter = require('./lib/reports/format.js'); 21 | 22 | var SCREENSHOTS_DIR = 'public/tmp/screenshots/'; 23 | 24 | var checklist = [ 25 | require('./lib/checks/performance/number-requests'), require( 26 | './lib/checks/performance/redirects'), require( 27 | './lib/checks/performance/http-errors'), require( 28 | './lib/checks/performance/compression'), require( 29 | './lib/checks/responsive/doc-width'), require( 30 | './lib/checks/responsive/meta-viewport'), require( 31 | './lib/checks/responsive/screenshot'), require( 32 | './lib/checks/compatibility/flash-detection'), require( 33 | './lib/checks/compatibility/css-prefixes'), require( 34 | './lib/checks/interactions/alert'), require( 35 | './lib/checks/integration/manifest') 36 | ]; 37 | 38 | var logs = { 39 | currentState: { 40 | clients: 0, 41 | validations: 0 42 | }, 43 | history: { 44 | startDate: new Date(), 45 | clients: 0, 46 | validations: 0 47 | } 48 | }; 49 | 50 | /* 51 | * Job Manager 52 | * Manage a job queue to avoid a server overload 53 | */ 54 | var maxJobs = 15; 55 | var jobQueue = []; 56 | var currentJobs = 0; 57 | 58 | function newJob(checker, options) { 59 | if (currentJobs < maxJobs) { 60 | currentJobs++; 61 | checker.check(options); 62 | } else { 63 | addJobToQueue(checker, options); 64 | } 65 | } 66 | 67 | function addJobToQueue(checker, options) { 68 | jobQueue.push({ 69 | checker: checker, 70 | options: options 71 | }); 72 | options.events.emit('wait'); 73 | } 74 | 75 | function removeJobToQueue(jobIndex){ 76 | jobQueue.splice(jobIndex, 1); 77 | } 78 | 79 | function runNewJobFromQueue() { 80 | if (currentJobs < maxJobs) { 81 | currentJobs++; 82 | jobQueue[0].options.events.emit('jobStarted'); 83 | jobQueue[0].checker.check(jobQueue[0].options); 84 | removeJobToQueue(0); 85 | } else { 86 | return; 87 | } 88 | } 89 | 90 | function endJob(){ 91 | if (currentJobs > 0) 92 | currentJobs--; 93 | if(jobQueue.length > 0) 94 | runNewJobFromQueue(); 95 | } 96 | 97 | function removeDisconnectJobToQueue(checkId) { 98 | for(var i = 0; i < jobQueue.length - 1; i++) { 99 | if(jobQueue[i].options.id == checkId) { 100 | removeJobToQueue(i); 101 | } 102 | } 103 | } 104 | 105 | function init() { 106 | createFolder(SCREENSHOTS_DIR, clearScreenshotFolder); 107 | } 108 | 109 | function updateLogs(code, socket) { 110 | switch (code) { 111 | case 'NEW_CLIENT': 112 | logs.currentState.clients++; 113 | logs.history.clients++; 114 | break; 115 | case 'NEW_VALIDATION': 116 | logs.currentState.validations++; 117 | logs.history.validations++; 118 | break; 119 | case 'CLIENT_DECONNECTED': 120 | logs.currentState.clients--; 121 | break; 122 | case 'VALIDATION_ENDED': 123 | logs.currentState.validations--; 124 | break; 125 | } 126 | socket.broadcast.emit('logs', reportLogs()); 127 | } 128 | 129 | function reportLogs() { 130 | var str = fs.readFileSync(path.join(__dirname, '/lib/logs/logs.ejs'), 'utf8'); 131 | return ejs.render(str, logs); 132 | } 133 | 134 | function createFolder(path, cb) { 135 | mkdirp(path, function(err) { 136 | if (err) console.error(err); 137 | else cb(); 138 | }); 139 | } 140 | 141 | function unlinkFile(path) { 142 | fs.unlink(path, function(err) { 143 | if (err) throw err; 144 | }); 145 | } 146 | 147 | function unlinkScreenshot(filename) { 148 | unlinkFile(SCREENSHOTS_DIR + filename); 149 | } 150 | 151 | var clearScreenshotFolder = function() { 152 | fs.readdir(SCREENSHOTS_DIR, function(err, files) { 153 | files.forEach(function(name) { 154 | unlinkScreenshot(name); 155 | }); 156 | }); 157 | }; 158 | 159 | function displayTip(socket) { 160 | setTimeout(function() { 161 | fs.readdir("lib/tips", function(err, files) { 162 | var tip = "lib/tips/" + files[Math.floor(files.length * Math.random())]; 163 | fs.readFile(tip, { 164 | encoding: "utf-8" 165 | }, function(err, data) { 166 | if (err) { 167 | return; 168 | } 169 | formatter.format("
" + data + "
", "info", "tip", function(content) { 170 | socket.emit("tip", content); 171 | }); 172 | validProfiles = files; 173 | }); 174 | }); 175 | }, 1500); 176 | } 177 | 178 | function Sink() {} 179 | 180 | util.inherits(Sink, events.EventEmitter); 181 | 182 | app.set('views', __dirname + '/public'); 183 | 184 | app.set('view engine', 'ejs'); 185 | 186 | app.use(compress()); 187 | 188 | app.use(express.static('public')); 189 | 190 | app.get('/logs', function(req, res) { 191 | res.render('logs', logs); 192 | }); 193 | 194 | init(); 195 | 196 | io.on('connection', function(socket) { 197 | updateLogs('NEW_CLIENT', socket); 198 | socket.on('check', function(data) { 199 | var sink = new Sink(), 200 | checker = new Checker(), 201 | uid = uuid.v4(), 202 | screenshot = false; 203 | sink.on('ok', function(data) { 204 | socket.emit('ok', data); 205 | }); 206 | sink.on('warning', function(data) { 207 | socket.emit('warning', data); 208 | }); 209 | sink.on('err', function(data) { 210 | socket.emit('err', data); 211 | }); 212 | sink.on('screenshot', function(data) { 213 | screenshot = true; 214 | socket.emit('screenshot', data); 215 | }); 216 | sink.on('done', function() { 217 | socket.emit('done'); 218 | }); 219 | sink.on('end', function(data) { 220 | updateLogs('VALIDATION_ENDED', socket); 221 | endJob(); 222 | socket.emit('end', data); 223 | }); 224 | sink.on('wait', function(){ 225 | socket.emit('wait', jobQueue.length); 226 | }); 227 | sink.on('jobStarted', function(){ 228 | socket.emit('jobStarted'); 229 | }); 230 | sink.on('exception', function(data) { 231 | socket.emit('exception', data); 232 | }); 233 | socket.on('disconnect', function() { 234 | updateLogs('CLIENT_DECONNECTED', socket); 235 | removeDisconnectJobToQueue(uid); 236 | if (screenshot) { 237 | unlinkScreenshot(uid + '.png'); 238 | } 239 | }); 240 | insafe.check({ 241 | url: data.url, 242 | statusCodesAccepted: ["301", "404"] 243 | }).then(function(res){ 244 | console.log(res); 245 | if(res.status === false) { 246 | socket.emit('unsafeUrl', res.url); 247 | } else { 248 | socket.emit('start'); 249 | updateLogs('NEW_VALIDATION', socket); 250 | displayTip(socket); 251 | newJob(checker, { 252 | url: res.url, 253 | events: sink, 254 | sockets: socket, 255 | profile: data.profile, 256 | checklist: checklist, 257 | id: uid, 258 | SCREENSHOTS_DIR: SCREENSHOTS_DIR, 259 | lang: "en" 260 | }); 261 | } 262 | }).catch(function(err){ 263 | console.log(err); 264 | }); 265 | }); 266 | }); 267 | 268 | http.listen(3000, function() { 269 | console.log('listening on *:3000'); 270 | }); 271 | -------------------------------------------------------------------------------- /ideas-for-tips.md: -------------------------------------------------------------------------------- 1 | Ideas for tips that the checker could give to users: 2 | 3 | * Save to homescreen (manifest.json) 4 | * Manage offline (appcache, serviceworker) 5 | * Don't push users away to native 6 | 7 | -------------------------------------------------------------------------------- /lib/checker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | */ 4 | /** 5 | * @module checker 6 | * @requires path 7 | * @requires child_process 8 | * @requires url 9 | * @requires http 10 | * @requires headless 11 | * @requires ejs 12 | * @requires fs 13 | */ 14 | var path = require('path'), 15 | proc = require('child_process'), 16 | url = require('url'), 17 | http = require('http'), 18 | headless = require('headless'), 19 | ejs = require('ejs'), 20 | fs = require('fs'), 21 | browserEmulator = require('mobile-web-browser-emulator'), 22 | formatter = require('./reports/format.js'); 23 | 24 | var filters = rootRequire('lib/ejs-filters'); 25 | 26 | filters.loadFilters(ejs); 27 | 28 | /** 29 | * @class 30 | */ 31 | var Checker = function() {}; 32 | 33 | 34 | /** 35 | * @augments Checker 36 | * @param {Object} options 37 | * @param {String} options.url URL for a document to load 38 | * @param {String} options.profile profile of the device selected 39 | * @param {Array} options.checklist array of checks to do 40 | * @param {} options.events where to send the various events 41 | * @param {Number} options.id generated id to save an unique screenshot 42 | */ 43 | Checker.prototype.check = function(options) { 44 | if (!options.events) return this.throw( 45 | "The events option is required for reporting."); 46 | if (!options.url) return this.throw(options.events, 47 | "Without url there is nothing to check."); 48 | if (!options.checklist) return this.throw(options.events, 49 | "Without checklist there is nothing to check"); 50 | if (!options.id) return this.throw(options.events, 51 | "Without id, the app can't generate screenshot"); 52 | 53 | var self = this, 54 | sink = options.events, 55 | checklist = options.checklist, 56 | resume = { 57 | ok: 0, 58 | issues: 0 59 | }; 60 | self.sink = sink; 61 | self.checklist = checklist; 62 | self.resume = resume; 63 | self.options = options; 64 | self.formatReport = options.formatReport || reportFormatter; 65 | self.id = options.id; 66 | 67 | fs.readdir(path.join(__dirname, "profiles"), function(err, files) { 68 | var validProfiles = files; 69 | if (!options.profile || validProfiles.indexOf(options.profile + ".json") === -1) { 70 | options.profile = 'default'; 71 | } 72 | var profile = require('./profiles/' + options.profile + '.json'); 73 | self.profile = profile; 74 | 75 | self.emulateBrowser(profile, options.url); 76 | }); 77 | 78 | }; 79 | /** 80 | * @augments Checker 81 | * @param {Object} profile device's profile 82 | * @param {Number} profile.width device's width 83 | * @param {Number} profile.height device's height 84 | * @param {String} url URL for a document to load 85 | */ 86 | Checker.prototype.emulateBrowser = function(profile, url) { 87 | var self = this; 88 | var opt = { 89 | display: { 90 | width: profile.width, 91 | height: profile.height 92 | } 93 | }; 94 | headless(opt, function(err, childProcess, servernum) { 95 | var Browser = browserEmulator.Browser; 96 | var browser = new Browser({ 97 | browserWidth: profile.width, 98 | browserHeight: profile.height, 99 | uaHeader: 'Mozilla/5.0 (Linux; Android 4.4.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/36.0.1025.133 Mobile Safari/535.19', 100 | displayServer: servernum, 101 | browsermobProxy: { 102 | port: 8080 103 | }, 104 | trackNetwork: true 105 | }); 106 | 107 | browser.open(url); 108 | browser.on('error', function(err) { 109 | console.log(err); 110 | browser.close(); 111 | childProcess.kill(); 112 | }); 113 | browser.on('screenshot', function(path) { 114 | self.sink.emit('screenshot', path.substring(7)); 115 | }); 116 | browser.on('done', function() { 117 | self.sink.emit('end'); 118 | }); 119 | self.launchChecklist(browser); 120 | browser.close(childProcess); 121 | }); 122 | }; 123 | /** 124 | * @augments Checker 125 | * @param {Object} browser browser object which emulate a mobile browser 126 | */ 127 | Checker.prototype.launchChecklist = function(browser) { 128 | for (var i = 0; i < this.checklist.length; i++) 129 | this.checklist[i].check(this, browser); 130 | }; 131 | /** 132 | * @augments Checker 133 | * @param {String} key represent the key of the send issue 134 | * @param {String} name name of the done check 135 | * @param {String} category of the concerned check 136 | * @param {Object} data optional data send with check result 137 | */ 138 | Checker.prototype.report = function(key, name, category, status, data) { 139 | var self = this; 140 | this.resume[name] = key; 141 | if (key == "ok") { 142 | this.resume.ok++; 143 | this.sink.emit('done'); 144 | } else { 145 | this.resume.issues++; 146 | var intl = require("intl"); 147 | if (!data) { 148 | data = {}; 149 | } 150 | this.formatReport(key, name, category, status, data, function (rep) { 151 | self.sink.emit('err', { 152 | "issue": rep, 153 | "status": status 154 | }); 155 | self.sink.emit('done'); 156 | }); 157 | } 158 | }; 159 | 160 | /** 161 | * @augments Checker 162 | * @param {String} key represent the key of the send issue 163 | * @param {String} name name of the done check 164 | * @param {String} category of the concerned check 165 | * @param {Object} data optional data send with check result 166 | */ 167 | function reportFormatter(key, name, category, status, data, cb) { 168 | var str = fs.readFileSync(path.join(__dirname, '/reports/' + category + '/' + 169 | name + '/' + key + '.ejs'), 'utf8'); 170 | data = data || {}; 171 | data.name = name; 172 | data.category = category; 173 | formatter.format(ejs.render(str, data), status, category, cb); 174 | } 175 | /** 176 | * @augments Checker 177 | * @param {} sink events 178 | * @param {String} msg error message to send to client console via sink event 179 | */ 180 | Checker.prototype.throw = function(sink, msg) { 181 | if (!sink) return console.error( 182 | "[BOOM] No event sink with which to report system errors.\nAlso: " + 183 | msg); 184 | sink.emit("exception", msg); 185 | }; 186 | 187 | exports.Checker = Checker; 188 | -------------------------------------------------------------------------------- /lib/checks/compatibility/css-prefixes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module css-prefixes 3 | * @requires css 4 | * @requires css-prefixes.json 5 | * @requires utils 6 | */ 7 | var cssParser = require('css'); 8 | var cssPrefixes = rootRequire('lib/css-prefixes.json'); 9 | 10 | var utils = rootRequire("lib/checks/utils"); 11 | 12 | 13 | var self = this; 14 | 15 | exports.name = "css-prefixes"; 16 | exports.category = "compatibility"; 17 | exports.check = function(checker, browser) { 18 | var missingPrefixEquivalent = {}; 19 | var mismatchPrefixEquivalent = {}; 20 | browser.on('har', function(har, done) { 21 | if (har && har.log && har.log.entries) { 22 | for (var i = 0; i < har.log.entries.length; i++) { 23 | var entry = har.log.entries[i]; 24 | if (entry.response.status === 200) { 25 | var mediaType = utils.mediaTypeName(entry.response.content 26 | .mimeType); 27 | if (mediaType === "text/css") { 28 | if (entry.response.content.text === undefined) { 29 | // not sure why that happens 30 | // but cssparser breaks on undefined 31 | continue; 32 | } 33 | 34 | var url = entry.request.url; 35 | var cssObj = cssParser.parse(entry.response.content 36 | .text, { 37 | silent: true 38 | }); 39 | for (var j = 0; j < cssObj.stylesheet.rules.length; j++) { 40 | var rule = cssObj.stylesheet.rules[j]; 41 | var prefixedProperties = {}; 42 | var unprefixedProperties = {}; 43 | if (rule.declarations) { 44 | for (var k = 0; k < rule.declarations.length; k++) { 45 | var decl = rule.declarations[k]; 46 | if (decl.property && hasprefix(decl.property)) { 47 | var unprefixed = unprefix(decl.property); 48 | if (!prefixedProperties[unprefixed]) { 49 | prefixedProperties[unprefixed] = {}; 50 | } 51 | prefixedProperties[unprefixed][decl 52 | .property 53 | ] = decl; 54 | } else { 55 | unprefixedProperties[decl.property] = 56 | decl; 57 | } 58 | } 59 | } 60 | var prefixed = Object.keys(prefixedProperties); 61 | for (var p = 0; p < prefixed.length; p++) { 62 | // add the unprefixed decl if it exists 63 | if (unprefixedProperties[prefixed[p]]) { 64 | prefixedProperties[prefixed[p]][ 65 | prefixed[p] 66 | ] = unprefixedProperties[prefixed[p]]; 67 | } 68 | var props = prefixedProperties[prefixed[p]]; 69 | var prefixedNames = Object.keys(props); 70 | var ref = props[prefixedNames[0]].value; 71 | var misMatchFound = false; 72 | for (var n = 1; n < prefixedNames.length; n++) { 73 | // are the various unprefixed decl the same? 74 | if (props[prefixedNames[n]].value !== 75 | ref) { 76 | misMatchFound = true; 77 | break; 78 | } 79 | } 80 | if (misMatchFound) { 81 | if (!mismatchPrefixEquivalent[url]) { 82 | mismatchPrefixEquivalent[url] = []; 83 | } 84 | mismatchPrefixEquivalent[url].push({ 85 | rule: stringify(rule), 86 | position: rule.position.start, 87 | prop: prefixed[p], 88 | decls: values(props).map( 89 | stringify) 90 | }); 91 | } 92 | var equivPrefixes = cssPrefixes[prefixed[p]] ? 93 | cssPrefixes[prefixed[p]].values : 94 | undefined; 95 | if (equivPrefixes) { 96 | var missing = []; 97 | for (var l = 0; l < equivPrefixes.length; l++) { 98 | if (!props[equivPrefixes[l]]) { 99 | missing.push(equivPrefixes[l]); 100 | } 101 | } 102 | if (missing.length) { 103 | if (!missingPrefixEquivalent[url]) { 104 | missingPrefixEquivalent[url] = []; 105 | } 106 | missingPrefixEquivalent[url].push({ 107 | rule: stringify(rule), 108 | position: rule.position.start, 109 | prop: prefixed[p], 110 | missing: missing, 111 | decls: values(props).map( 112 | stringify), 113 | value: values(props)[0].value 114 | }); 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | if (Object.keys(missingPrefixEquivalent).length) { 123 | checker.report("missing-prefixes", self.name, self.category, "warning",{ 124 | missingPrefixes: missingPrefixEquivalent 125 | }); 126 | } 127 | if (Object.keys(mismatchPrefixEquivalent).length) { 128 | checker.report("mismatching-prefixes", self.name, 129 | self.category, "warning",{ 130 | mismatchingPrefixes: mismatchPrefixEquivalent 131 | }); 132 | } 133 | } 134 | done(); 135 | }); 136 | }; 137 | 138 | function hasprefix(string) { 139 | return string.match(/^-[^-]*-/); 140 | } 141 | 142 | function unprefix(string) { 143 | return string.replace(/^-[^-]*-/, ''); 144 | } 145 | 146 | function stringify(obj) { 147 | var stringified; 148 | switch (obj.type) { 149 | case 'rule': 150 | stringified = obj.selectors.join(', ') + ' {' + '\n'; 151 | for (var i = 0; i < obj.declarations.length; i++) { 152 | stringified += ' ' + stringify(obj.declarations[i]); 153 | } 154 | stringified += "}"; 155 | break; 156 | case 'declaration': 157 | stringified = obj.property + ': ' + 158 | obj.value + ';\n'; 159 | break; 160 | } 161 | return stringified; 162 | } 163 | 164 | function values(obj) { 165 | var vals = Object.keys(obj).map(function(key) { 166 | return obj[key]; 167 | }); 168 | return vals; 169 | } 170 | -------------------------------------------------------------------------------- /lib/checks/compatibility/flash-detection.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module flash-detection 3 | */ 4 | 5 | var self = this; 6 | 7 | exports.name = "flash-detection"; 8 | exports.category = "compatibility"; 9 | exports.check = function(checker, browser) { 10 | 11 | // TODO: should we detect this at the network level instead? 12 | browser.do(function(driver) { 13 | return driver.findElements(browser.webdriver.By.tagName('script')). 14 | then(function(scripts) { 15 | browser.webdriver.promise.map( 16 | scripts, 17 | function(el) { 18 | return el.getAttribute("src"); 19 | } 20 | ).then(function(srcs) { 21 | for (var i = 0; i < srcs.length; i++) { 22 | if (srcs[i].indexOf('swfobject.js') !== -1) { 23 | checker.report("swfobject-lib-detected", 24 | self.name, 25 | self.category, 26 | "error"); 27 | } 28 | } 29 | }); 30 | }); 31 | }); 32 | 33 | browser.do(function(driver) { 34 | return driver.findElements(browser.webdriver.By.tagName('embed')). 35 | then(function(embeds) { 36 | browser.webdriver.promise.map( 37 | embeds, 38 | function(el) { 39 | return el.getAttribute("src"); 40 | } 41 | ).then(function(srcs) { 42 | for (var i = 0; i < srcs.length; i++) { 43 | if (srcs[i].indexOf('.swf') !== -1) { 44 | checker.report("swf-file-detected", 45 | self.name, self.category, 46 | "error"); 47 | } 48 | } 49 | }); 50 | }); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/checks/integration/manifest.js: -------------------------------------------------------------------------------- 1 | // check if there is a JSON manifest 2 | // and if there is, if it is valid 3 | /** 4 | * @module manifest 5 | * @requires schema-validator 6 | */ 7 | var SchemaValidator = require('jsonschema').Validator; 8 | var manifestschema = require('./web-manifest.json'); 9 | var request = require("request"); 10 | var url = require("url"); 11 | 12 | var self = this; 13 | 14 | exports.name = "manifest"; 15 | exports.category = "integration"; 16 | exports.check = function(checker, browser) { 17 | function checkUrl(urlRef, baseUrl, type) { 18 | var absUrl; 19 | try { 20 | absUrl = url.resolve(baseUrl, urlRef); 21 | } catch (e) { 22 | checker.report("invalidurl_property", self.name, 23 | self.category, "warning", { 24 | manifest: baseUrl, 25 | property: type, 26 | url: urlRef, 27 | error: e.name 28 | }); 29 | return; 30 | } 31 | request(absUrl, function (error, response, body) { 32 | if (error) { 33 | checker.report("networkerror_property", self.name, 34 | self.category, "warning", { 35 | manifest: baseUrl, 36 | property: type, 37 | url: absUrl, 38 | error: error 39 | }); 40 | 41 | return; 42 | } 43 | if (response.statusCode >= 400) { 44 | checker.report("httperror_property" , self.name, 45 | self.category, "warning", { 46 | manifest: baseUrl, 47 | property: type, 48 | url: absUrl, 49 | error: response.statusCode 50 | }); 51 | return; 52 | } 53 | }); 54 | } 55 | 56 | 57 | browser.do(function(driver) { 58 | return driver.findElements(browser.webdriver.By.css('link[rel="manifest"]')) 59 | .then(function(manifestLinks) { 60 | // return all the manifests found 61 | browser.webdriver.promise.map( 62 | manifestLinks, 63 | function(el) { 64 | return el.getAttribute("href"); 65 | } 66 | ).then(function(manifests) { 67 | if (!manifests || !manifests.length) { 68 | return; 69 | } 70 | var manifest = manifests[0]; 71 | if (manifests.length > 1) { 72 | checker.report("multiple-manifests", self.name, 73 | self.category, "warning", { 74 | links: manifests.slice(1) 75 | }); 76 | } 77 | request(manifests[0], function (error, response, body) { 78 | if (error) { 79 | checker.report("httperror", self.name, 80 | self.category, "warning", { 81 | manifest: manifest, 82 | error: error 83 | }); 84 | return; 85 | } 86 | if (response.statusCode >= 400) { 87 | checker.report("httperror", self.name, 88 | self.category, "warning", { 89 | manifest: manifest, 90 | httperror: response.statusCode 91 | }); 92 | return; 93 | } 94 | var data; 95 | try { 96 | data = JSON.parse(body); 97 | } catch (e) { 98 | checker.report("jsonerror", self.name, 99 | self.category, "warning", { 100 | manifest: manifest, 101 | error: {name: e.name, message: e.message} 102 | }); 103 | return; 104 | } 105 | var validator = new SchemaValidator(); 106 | var check = validator.validate(data, manifestschema); 107 | if (check.errors.length > 0) { 108 | checker.report("jsonserror", self.name, 109 | self.category, "warning", { 110 | manifest: manifest, 111 | errors: check.errors 112 | }); 113 | } 114 | // Verify URLs are dereferencable 115 | if (data.start_url) { 116 | checkUrl(data.start_url, manifest, "start_url"); 117 | } 118 | if (data.icons && data.icons.length > 0) { 119 | data.icons.forEach( function (icon) { 120 | if (icon.src) { 121 | checkUrl(icon.src, manifest, "icon"); 122 | // TODO: more tests on icons 123 | // e.g. matching their size with sizes property? 124 | // ensuring that "typical" sizes are provided? 125 | } 126 | }); 127 | } 128 | }); 129 | }); 130 | }); 131 | }); 132 | }; 133 | 134 | -------------------------------------------------------------------------------- /lib/checks/integration/web-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "JSON schema for Web Application manifest files", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | 5 | "type": "object", 6 | 7 | "properties": { 8 | "name": { 9 | "description": "The name of the web application.", 10 | "type": "string" 11 | }, 12 | "short_name": { 13 | "description": "A short version of the name of the web application.", 14 | "type": "string" 15 | }, 16 | "icons": { 17 | "description": "The icons member is an array of icon objects that can serve as iconic representations of the web application in various contexts.", 18 | "type": "array", 19 | "items": { 20 | "$ref": "#/definitions/icon" 21 | } 22 | }, 23 | "display": { 24 | "description": "The item represents the developer's preferred display mode for the web application.", 25 | "enum": [ "fullscreen", "standalone", "minimal-ui", "browser" ] 26 | }, 27 | "orientation": { 28 | "description": "The orientation member is a string that serves as the default orientation for all top-level browsing contexts of the web application.", 29 | "enum": [ "any", "natural", "landscape", "portrait", "portrait-primary", "portrait-secondary", "landscape-primary", "landscape-secondary" ] 30 | }, 31 | "start_url": { 32 | "description": "Represents the URL that the developer would prefer the user agent load when the user launches the web application.", 33 | "type": "string" 34 | } 35 | }, 36 | 37 | "definitions": { 38 | "icon": { 39 | "type": "object", 40 | "properties": { 41 | "density": { 42 | "description": "The density member of an icon is the device pixel density for which this icon was designed.", 43 | "type": "number", 44 | "default": 1.0 45 | }, 46 | "sizes": { 47 | "description": "The sizes member is a string consisting of an unordered set of unique space-separated tokens which are ASCII case-insensitive that represents the dimensions of an icon for visual media.", 48 | "oneOf": [ 49 | { 50 | "type": "string", 51 | "pattern": "^[0-9 x]+$" 52 | }, 53 | { 54 | "enum": [ "any" ] 55 | } 56 | ] 57 | }, 58 | "src": { 59 | "description": "The src member of an icon is a URL from which a user agent can fetch the icon's data.", 60 | "type": "string" 61 | }, 62 | "type": { 63 | "description": "The type member of an icon is a hint as to the media type of the icon.", 64 | "type": "string", 65 | "pattern": "^[\\sa-z0-9\\-+;\\.=\\/]+$" 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/checks/interactions/alert.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module alert 3 | */ 4 | //check number of requests 5 | exports.name = "alert"; 6 | exports.category = "interactions"; 7 | 8 | var self = this; 9 | 10 | exports.check = function(checker, browser) { 11 | browser.once('alert', function(text) { 12 | checker.report("alert-detected", self.name, self.category, "error", { 13 | text: text 14 | }); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/checks/interactions/touchscreen-target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module touchscreen-target 3 | */ 4 | 5 | var self = this; 6 | 7 | exports.name = "touchscreen-target"; 8 | exports.category = "interactions"; 9 | 10 | exports.check = function(checker, browser) { 11 | browser.do(function(driver) { 12 | return driver.executeScript(function(args) { 13 | for (var index in args.tags) { 14 | args.components.push(document.documentElement.getElementsByTagName( 15 | args.tags[index])); 16 | } 17 | return args.components; 18 | }, { 19 | tags: ["button", "a", "input"], 20 | components: [] 21 | }).then(function(data) { 22 | var targets = { 23 | tag: [], 24 | size: [], 25 | location: [], 26 | outerHtml: [] 27 | }; 28 | var addTo = function(target) { 29 | return function(x) { 30 | targets[target].push(x); 31 | }; 32 | }; 33 | var addTag = addTo("tag"); 34 | var addSize = addTo("size"); 35 | var addLocation = addTo("location"); 36 | var addOuterHtml = addTo("outerHtml"); 37 | for (var i in data) { 38 | for (var j in data[i]) { 39 | data[i][j].getTagName().then(addTag); 40 | data[i][j].getSize().then(addSize); 41 | data[i][j].getLocation().then(addLocation); 42 | data[i][j].getOuterHtml().then(addOuterHtml); 43 | } 44 | } 45 | return targets; 46 | }).then(function(targets) { 47 | var smallTargetsDetected = { 48 | tag: [], 49 | size: { 50 | width: [], 51 | height: [] 52 | }, 53 | location: [], 54 | outerHtml: [] 55 | }; 56 | if (checker.resume["meta-viewport"]) { 57 | 58 | } else { 59 | for (var index in targets.size) { 60 | if ((targets.size[index].width < 48 || targets.size[ 61 | index].height < 48) && targets.size[ 62 | index].width !== 0 && targets.size[index] 63 | .height !== 0) { 64 | smallTargetsDetected.tag.push(targets.tag[ 65 | index]); 66 | smallTargetsDetected.size.width.push( 67 | targets.size[index].width); 68 | smallTargetsDetected.size.height.push( 69 | targets.size[index].height); 70 | smallTargetsDetected.location.push(targets.location[ 71 | index]); 72 | if (targets.outerHtml[index].length > 40) { 73 | smallTargetsDetected.outerHtml.push( 74 | targets.outerHtml[index].substring( 75 | 0, 39) + '...'); 76 | } else { 77 | smallTargetsDetected.outerHtml.push( 78 | targets.outerHtml[index]); 79 | } 80 | } 81 | } 82 | } 83 | return smallTargetsDetected; 84 | }).then(function(smallTargetsDetected) { 85 | if (smallTargetsDetected.size.width.length > 0) { 86 | checker.report("too-small-touchscreen-target", 87 | self.name, self.category, "warning", smallTargetsDetected); 88 | return; 89 | } else { 90 | checker.report("ok", self.name, self.category); 91 | } 92 | }); 93 | }); 94 | }; -------------------------------------------------------------------------------- /lib/checks/performance/compression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module compression 3 | * @requires utils 4 | */ 5 | // detect uncompressed textual resources 6 | exports.name = "compression"; 7 | exports.category = "performance"; 8 | 9 | var utils = rootRequire("lib/checks/utils"); 10 | 11 | var self = this; 12 | 13 | exports.check = function(checker, browser) { 14 | var calculatingCompression = 0; 15 | var calculatedCompression = 0; 16 | var compressable = []; 17 | 18 | browser.on('har', function(har, done) { 19 | function reportCompressionSaving(err, compressableItem) { 20 | calculatedCompression++; 21 | // Only report compression savings > 1000 bytes 22 | if (compressableItem.diff > 1000) { 23 | compressable.push(compressableItem); 24 | } 25 | if (calculatedCompression === calculatingCompression) { 26 | if (compressable.length) { 27 | compressable.sort(function(a, b) { 28 | return b.diff - a.diff; 29 | }); 30 | var saving = compressable.reduce(function(prev, a) { 31 | return prev + a.diff; 32 | }, 0); 33 | checker.report("resources-could-be-compressed", self.name, 34 | self.category, "warning", { 35 | number: compressable.length, 36 | compressable: compressable, 37 | saving: saving 38 | }); 39 | } 40 | } 41 | done(); 42 | } 43 | 44 | if (har && har.log && har.log.entries) { 45 | for (var i = 0; i < har.log.entries.length; i++) { 46 | var entry = har.log.entries[i]; 47 | if (isCompressableResponse(entry.response) && ! 48 | isCompressedResponse(entry.response) && 49 | shouldBeCompressedResponse(entry.response)) { 50 | calculatingCompression++; 51 | calculateCompressionSaving(entry.request.url, entry.response, 52 | reportCompressionSaving); 53 | } 54 | } 55 | 56 | } 57 | 58 | if (calculatingCompression === 0) { 59 | done(); 60 | } 61 | }); 62 | }; 63 | 64 | 65 | function isCompressableResponse(response) { 66 | var compressableMediaTypes = ['text/html', 'application/json', 67 | 'image/svg+xml', 'text/css', 68 | 'text/javascript', 'application/javascript', 69 | 'text/plain', 'text/xml', 'application/xml' 70 | ]; 71 | 72 | var mediaType = utils.mediaTypeName(response.content.mimeType); 73 | return (response.status === 200 && compressableMediaTypes.indexOf(mediaType) !== 74 | -1); 75 | } 76 | 77 | function isCompressedResponse(response) { 78 | var contentEncoding = utils.findHeader(response.headers, "Content-Encoding"); 79 | var transferEncoding = utils.findHeader(response.headers, 80 | "Transfer-Encoding"); 81 | var hasCompressedContentEncoding = (contentEncoding && 82 | (contentEncoding.toLowerCase() === 'gzip' || contentEncoding.toLowerCase() === 83 | 'deflate') 84 | ); 85 | var hasCompressedTransferEncoding = (transferEncoding && 86 | (transferEncoding.toLowerCase() === 'gzip' || transferEncoding.toLowerCase() === 87 | 'deflate') 88 | ); 89 | return hasCompressedTransferEncoding || hasCompressedContentEncoding; 90 | } 91 | 92 | function shouldBeCompressedResponse(response) { 93 | return response.bodySize > 512; 94 | } 95 | 96 | function calculateCompressionSaving(url, response, cb) { 97 | var zlib = require("zlib"); 98 | var gzipped = zlib.gzip(response.content.text, function(err, buffer) { 99 | cb(err, { 100 | url: url, 101 | origSize: response.bodySize, 102 | diff: response.bodySize - buffer.length 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /lib/checks/performance/exif.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module exif 3 | * @requires utils 4 | */ 5 | // detect JPEG images with EXIF metadata 6 | exports.name = "exif"; 7 | exports.category = "performance"; 8 | 9 | var utils = rootRequire("lib/checks/utils"); 10 | var ImageHeaders = require("image-headers"); 11 | var binary_reader = require("binary-reader"); 12 | 13 | var self = this; 14 | 15 | exports.check = function(checker, browser) { 16 | var calculatingMinification = 0; 17 | var calculatedMinification = 0; 18 | var minifiable = []; 19 | 20 | browser.on('har', function(har, done) { 21 | var image_headers = new ImageHeaders(); 22 | if (har && har.log && har.log.entries) { 23 | for (var i = 0; i < har.log.entries.length; i++) { 24 | var entry = har.log.entries[i]; 25 | if (isJPEG(entry.response)) { 26 | calculatingMinification++; 27 | var image = new Buffer(entry.response.content.text, 'base64'); 28 | var i = 0; 29 | while (i < image.length) { 30 | image_headers.add_bytes(image.readUInt8(i, true)); 31 | if (image_headers.finished) { 32 | break; 33 | } 34 | i++; 35 | } 36 | image_headers.finish(function(err, headers) { 37 | var exif_data_length = headers.exif_buffer ? headers.exif_buffer.length : 0; 38 | reportMinificationSaving(err, 39 | { 40 | url: entry.request.url, 41 | origSize: entry.response.bodySize, 42 | diff: exif_data_length 43 | }); 44 | }); 45 | } 46 | } 47 | } 48 | 49 | if (calculatingMinification === 0) { 50 | done(); 51 | } 52 | 53 | function reportMinificationSaving(err, minifiableItem) { 54 | calculatedMinification++; 55 | // Only report minification savings > 1000 bytes 56 | if (minifiableItem.diff > 1000) { 57 | minifiable.push(minifiableItem); 58 | } 59 | if (calculatedMinification === calculatingMinification) { 60 | if (minifiable.length) { 61 | minifiable.sort(function(a, b) { 62 | return b.diff - a.diff; 63 | }); 64 | var saving = minifiable.reduce(function(prev, a) { 65 | return prev + a.diff; 66 | }, 0); 67 | checker.report("images-could-be-unexified", self.name, 68 | self.category, "warning", { 69 | number: minifiable.length, 70 | minifiable: minifiable, 71 | saving: saving 72 | }); 73 | } 74 | } 75 | done(); 76 | } 77 | }); 78 | 79 | 80 | }; 81 | 82 | 83 | function isJPEG(response) { 84 | var mediaType = utils.mediaTypeName(response.content.mimeType); 85 | return (response.status === 200 && mediaType === "image/jpeg") 86 | } 87 | 88 | -------------------------------------------------------------------------------- /lib/checks/performance/http-errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module http-errors 3 | * @requires url 4 | */ 5 | // detect HTTP errors 6 | exports.name = "http-errors"; 7 | exports.category = "performance"; 8 | 9 | var self = this; 10 | 11 | exports.check = function(checker, browser) { 12 | var urlparse = require("url"); 13 | 14 | browser.on('har', function(har, done) { 15 | if (har && har.log && har.log.entries) { 16 | var errors = []; 17 | var faviconNotFound = false; 18 | for (var i = 0; i < har.log.entries.length; i++) { 19 | var entry = har.log.entries[i]; 20 | if (entry.response.status >= 400) { 21 | var urlObj = urlparse.parse(entry.request.url); 22 | if (urlObj.path === '/favicon.ico' && entry.response.status === 23 | 404) { 24 | faviconNotFound = entry.request.url; 25 | } else { 26 | errors.push({ 27 | url: entry.request.url, 28 | status: entry.response.status, 29 | statusText: entry.response.statusText 30 | }); 31 | } 32 | } 33 | } 34 | if (errors.length) { 35 | checker.report("http-errors-detected", self.name, 36 | self.category, "error", { 37 | number: errors.length, 38 | errors: errors 39 | }); 40 | } 41 | if (faviconNotFound) { 42 | checker.report("favicon", self.name, self.category, "warning", { 43 | url: faviconNotFound 44 | }); 45 | } 46 | } 47 | done(); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /lib/checks/performance/number-requests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module number-requests 3 | */ 4 | //check number of requests 5 | exports.name = "number-requests"; 6 | exports.category = "performance"; 7 | 8 | var self = this; 9 | 10 | exports.check = function(checker, browser) { 11 | browser.on('har', function(har, done) { 12 | if (har && har.log && har.log.entries) { 13 | checker.report("info-number-requests", self.name, self.category, "info", { 14 | number: har.log.entries.length, 15 | entries: har.log.entries.map( 16 | function(e) { return { url: e.request.url, 17 | status: e.response.status, 18 | mimeType: e.response.content.mimeType, 19 | bodySize: e.response.bodySize, 20 | time: e.time 21 | }; 22 | }) 23 | }); 24 | } 25 | done(); 26 | }); 27 | }; -------------------------------------------------------------------------------- /lib/checks/performance/redirects.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module redirects 3 | * @requires url 4 | * @requires utils 5 | */ 6 | // detect redirects 7 | exports.name = "redirects"; 8 | exports.category = "performance"; 9 | 10 | var urlparse = require("url"); 11 | 12 | var utils = rootRequire("lib/checks/utils"); 13 | 14 | var self = this; 15 | 16 | exports.check = function(checker, browser) { 17 | var redirectCodes = [301, 302, 303, 307]; 18 | browser.on('har', function(har, done) { 19 | if (har && har.log && har.log.entries) { 20 | var redirects = []; 21 | var mainUrl = har.log.entries[0].request.url; 22 | // we ignore the first entry (the page itself) 23 | for (var i = 1; i < har.log.entries.length; i++) { 24 | var entry = har.log.entries[i]; 25 | if (redirectCodes.indexOf(entry.response.status) !== -1) { 26 | var redirectURL = entry.response.redirectURL; 27 | 28 | // browsermob-proxy seems to fail to resolve 29 | // scheme-relative url 30 | if (!redirectURL) { 31 | redirectURL = urlparse.resolve(entry.request.url, 32 | utils.findHeader(entry.response.headers, 33 | "location")); 34 | } 35 | 36 | redirects.push({ 37 | from: entry.request.url, 38 | to: redirectURL, 39 | wastedBW: entry.request.bodySize + entry.request 40 | .headersSize + entry.response.headersSize + 41 | entry.response.bodySize, 42 | latency: entry.time 43 | }); 44 | } 45 | } 46 | redirects.sort(function(a, b) { 47 | return utils.localUrlCompare(mainUrl)(a.from, b.from); 48 | }); 49 | if (redirects.length) { 50 | var totalWastedBW = redirects.reduce(function(prev, r) { 51 | return prev + r.wastedBW; 52 | }, 0); 53 | var totalLatency = redirects.reduce(function(prev, r) { 54 | return prev + r.latency; 55 | }, 0); 56 | 57 | checker.report("redirects-encountered", self.name, 58 | self.category, "warning", { 59 | number: redirects.length, 60 | redirects: redirects, 61 | totalWastedBW: totalWastedBW, 62 | totalLatency: totalLatency 63 | }); 64 | } else { 65 | checker.sink.emit("done"); 66 | } 67 | 68 | } 69 | done(); 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /lib/checks/responsive/doc-width.js: -------------------------------------------------------------------------------- 1 | //check width of document (DOM) 2 | /** 3 | * @module doc-width 4 | */ 5 | exports.name = "doc-width"; 6 | exports.category = "responsive"; 7 | 8 | var self = this; 9 | 10 | exports.check = function(checker, browser) { 11 | browser.do(function(driver) { 12 | return driver.executeScript(function() { 13 | return [window.document.documentElement.scrollWidth, window.document 14 | .documentElement.clientWidth]; 15 | }).then(function(data) { 16 | var scrollWidth = data[0]; 17 | var clientWidth = data[1]; 18 | if (scrollWidth > clientWidth) { 19 | checker.report("doc-width-too-large", self.name, 20 | self.category, "warning"); 21 | } else { 22 | checker.sink.emit('done'); 23 | } 24 | }); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/checks/responsive/fonts-size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module fonts-size 3 | */ 4 | 5 | var self = this; 6 | 7 | exports.name = "fonts-size"; 8 | exports.category = "responsive"; 9 | 10 | exports.check = function(checker, browser) { 11 | browser.do(function(driver) { 12 | return driver.executeScript(function(args) { 13 | for (var index in args.tags) { 14 | args.components.push(document.documentElement.getElementsByTagName( 15 | args.tags[index])); 16 | } 17 | return args.components; 18 | }, { 19 | tags: ["p", "button", "input", "h1", "h2", "h3", "h4", 20 | "h5", "h6", "li", "a" 21 | ], 22 | components: [] 23 | }).then(function(data) { 24 | var fonts = { 25 | tag: [], 26 | size: [], 27 | location: [], 28 | content: [] 29 | }; 30 | var addTag = function(name) { 31 | fonts.tag.push(name); 32 | }; 33 | var addSize = function(size) { 34 | fonts.size.push(parseFloat(size.replace("px", ""))); 35 | }; 36 | var addLocation = function(location) { 37 | fonts.location.push(location); 38 | }; 39 | var addContent = function(text) { 40 | fonts.content.push(text); 41 | }; 42 | for (var i in data) { 43 | for (var j in data[i]) { 44 | data[i][j].getTagName().then(addTag); 45 | data[i][j].getCssValue("font-size").then(addSize); 46 | data[i][j].getLocation().then(addLocation); 47 | data[i][j].getText().then(addContent); 48 | } 49 | } 50 | return fonts; 51 | }).then(function(fonts) { 52 | 53 | var smallFontsDetected = { 54 | tag: [], 55 | size: [], 56 | location: [], 57 | content: [] 58 | }; 59 | if (checker.resume["meta-viewport"]) { 60 | //TODO : manage with wrong meta-viewport declaration 61 | } else { 62 | for (var index in fonts.size) { 63 | if (fonts.size[index] < 12 && fonts.content[ 64 | index] !== '') { 65 | smallFontsDetected.tag.push(fonts.tag[index]); 66 | smallFontsDetected.size.push(fonts.size[ 67 | index]); 68 | smallFontsDetected.location.push(fonts.location[ 69 | index]); 70 | if (fonts.content[index].length > 40) { 71 | smallFontsDetected.content.push(fonts.content[ 72 | index].substring(0, 39) + '...'); 73 | } else { 74 | smallFontsDetected.content.push(fonts.content[ 75 | index]); 76 | } 77 | } 78 | } 79 | } 80 | return smallFontsDetected; 81 | }).then(function(smallFontsDetected) { 82 | if (smallFontsDetected.size.length > 0) { 83 | checker.report("too-small-font-size", self.name, 84 | self.category, "warning",smallFontsDetected); 85 | return; 86 | } else { 87 | checker.report("ok", self.name, self.category); 88 | } 89 | }); 90 | }); 91 | }; -------------------------------------------------------------------------------- /lib/checks/responsive/meta-viewport.js: -------------------------------------------------------------------------------- 1 | //viewport influence skill-responsive of mobile website 2 | //check if the viewport is correctly declared 3 | /** 4 | * @module meta-viewport 5 | * @requires metaviewport-parser 6 | */ 7 | var metaparser = require('metaviewport-parser'); 8 | 9 | var wellknownMetaviewportProperties = ["target-densitydpi"]; 10 | 11 | var self = this; 12 | 13 | exports.name = "meta-viewport"; 14 | exports.category = "responsive"; 15 | exports.check = function(checker, browser) { 16 | browser.do(function(driver) { 17 | return driver.findElements(browser.webdriver.By.css('meta[name="viewport"]')) 18 | .then(function(viewportDecls) { 19 | // return all the metaviewports found 20 | browser.webdriver.promise.map( 21 | viewportDecls, 22 | function(el) { 23 | return el.getAttribute("content"); 24 | } 25 | ).then( 26 | function(metaviewports) { 27 | if (metaviewports === undefined || 28 | metaviewports.length === 0) { 29 | checker.report("no-viewport-declared", 30 | self.name, self.category, "error"); 31 | return; 32 | } 33 | if (metaviewports.length > 1) { 34 | checker.report("several-viewports-declared", 35 | self.name, 36 | self.category, "warning"); 37 | } 38 | // the last one is the one used 39 | var actualViewport = metaviewports[ 40 | metaviewports.length - 1]; 41 | var parsedViewport = metaparser.parseMetaViewPortContent( 42 | actualViewport); 43 | for (var prop in parsedViewport.invalidValues) { 44 | checker.report("invalid-viewport-value", 45 | self.name, 46 | self.category, "error", { 47 | property: prop, 48 | value: parsedViewport.invalidValues[ 49 | prop], 50 | validValues: metaparser.expectedValues[ 51 | prop].join(", ") 52 | }); 53 | } 54 | if (!parsedViewport.validProperties.width && 55 | !parsedViewport.validProperties[ 56 | "initial-scale"]) { 57 | checker.report("content-viewport-missed", 58 | self.name, "error", 59 | self.category); 60 | } else { 61 | if (parsedViewport.validProperties.width) { 62 | if (parsedViewport.validProperties.width === 63 | "device-width" || 64 | parsedViewport.validProperties.width === 65 | "device-height") { 66 | //OK 67 | checker.sink.emit('done'); 68 | } else { 69 | checker.report("hardcoded-viewport-width", 70 | self.name, self.category, "warning"); 71 | } 72 | } 73 | if (parsedViewport.validProperties[ 74 | "initial-scale"]) { 75 | //OK 76 | checker.sink.emit('done'); 77 | } 78 | if (parsedViewport.validProperties[ 79 | "user-scalable"] === "no") { 80 | checker.report("users-are-prevented-to-zoom", 81 | self.name, self.category, "warning"); 82 | } 83 | var unknownProperties = Object.keys( 84 | parsedViewport.unknownProperties); 85 | var nonstandardProperties = 86 | unknownProperties.filter( 87 | function(i) { 88 | return wellknownMetaviewportProperties 89 | .indexOf(i) !== -1; 90 | } 91 | ); 92 | var unrecognizedProperties = 93 | unknownProperties.filter( 94 | function(i) { 95 | return wellknownMetaviewportProperties 96 | .indexOf(i) === -1; 97 | } 98 | ); 99 | if (nonstandardProperties.length) { 100 | checker.report( 101 | "non-standard-viewport-parameter-declared", 102 | self.name, self.category, "warning", 103 | {nonstandardProperties: nonstandardProperties}); 104 | } 105 | if (unrecognizedProperties.length) { 106 | checker.report( 107 | "unknow-viewport-parameter-declared", 108 | self.name, self.category, "error"); 109 | } 110 | } 111 | 112 | } 113 | ); 114 | }); 115 | }); 116 | }; -------------------------------------------------------------------------------- /lib/checks/responsive/screenshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module screenshot 3 | */ 4 | 5 | var self = this; 6 | 7 | exports.name = "meta-viewport"; 8 | exports.category = "responsive"; 9 | exports.check = function(checker, browser) { 10 | browser.takeScreenshot(checker.options.SCREENSHOTS_DIR + checker.id + ".png"); 11 | }; -------------------------------------------------------------------------------- /lib/checks/utils.js: -------------------------------------------------------------------------------- 1 | var domainNameParser = require("effective-domain-name-parser"); 2 | var urlparse = require("url"); 3 | var mediaTypeParser = require('media-type'); 4 | 5 | exports.localUrlCompare = function(mainUrl) { 6 | var mainDNS = domainNameParser.parse(urlparse.parse(mainUrl).hostname); 7 | var mainName = mainDNS.sld + '.' + mainDNS.tld; 8 | 9 | return function(a, b) { 10 | var comparison = compareDomainName(urlparse.parse(a).hostname, 11 | urlparse.parse(b).hostname); 12 | if (comparison === 0) { 13 | return cmp(a, b); 14 | } 15 | return comparison; 16 | }; 17 | 18 | // sort by main domain name (e.g. w3.org) 19 | // put first the domain of the original request 20 | function compareDomainName(a, b) { 21 | var aDNS = domainNameParser.parse(a); 22 | var bDNS = domainNameParser.parse(b); 23 | var aName = aDNS.sld + "." + aDNS.tld; 24 | var bName = bDNS.sld + "." + bDNS.tld; 25 | if (aName === bName) { 26 | return 0; 27 | } 28 | if (aName === mainName) { 29 | return -1; 30 | } 31 | if (bName === mainName) { 32 | return 1; 33 | } 34 | return cmp(aName, bName); 35 | } 36 | 37 | function cmp(a, b) { 38 | var aStr = a.toString(); 39 | var bStr = b.toString(); 40 | return (aStr === bStr ? 0 : (aStr < bStr ? -1 : 1)); 41 | } 42 | }; 43 | 44 | exports.findHeader = function(headers, name, last) { 45 | var matchingHeaders = headers.filter(function(i) { 46 | return i.name.toLowerCase() === name.toLowerCase(); 47 | }); 48 | if (!matchingHeaders.length) { 49 | return; 50 | } 51 | if (last) { 52 | return matchingHeaders[matchingHeaders.length - 1].value; 53 | } 54 | return matchingHeaders[0].value; 55 | }; 56 | 57 | exports.mediaTypeName = function(mime) { 58 | var media = mediaTypeParser.fromString(mime); 59 | if (media.isValid()) { 60 | return media.type + '/' + media.subtype + (media.hasSuffix() ? '+' + 61 | media.suffix : ''); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /lib/ejs-filters.js: -------------------------------------------------------------------------------- 1 | exports.loadFilters = function(ejs) { 2 | 3 | // provide a shortened readable version of a URL 4 | ejs.filters.ellipsizeUrl = function(url, maxsize) { 5 | maxsize = maxsize || 40; 6 | if (url.length < maxsize) { 7 | // for consistency with other ellipsized URLs 8 | return url.replace(/^http:\/\//, ''); 9 | } 10 | 11 | var urlparse = require("url"); 12 | var urlObj = urlparse.parse(url); 13 | urlObj.port = undefined; 14 | urlObj.host = urlObj.hostname; 15 | urlObj.auth = undefined; 16 | urlObj.hash = undefined; 17 | var path = urlObj.path; 18 | urlObj.pathname = undefined; 19 | urlObj.search = undefined; 20 | urlObj.query = undefined; 21 | var urlStart = urlparse.format(urlObj); 22 | urlStart = urlStart.replace(/^http:\/\//, ''); 23 | return urlStart + path.slice(0, maxsize - urlStart.length - 1) + "…"; 24 | }; 25 | 26 | ejs.filters.number = function(number) { 27 | if (number !== null) { 28 | return number.toLocaleString(); 29 | } 30 | return null; 31 | }; 32 | 33 | ejs.filters.byteSize = function(number) { 34 | if (number > 1024) { 35 | var kb = Math.round(10 * number / 1024) / 10; 36 | return kb.toLocaleString() + " kB"; 37 | } 38 | return number.toLocaleString() + " B"; 39 | }; 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /lib/issues-scale.json: -------------------------------------------------------------------------------- 1 | { 2 | "compatibility": { 3 | "css-prefixes": { 4 | "mismatching-prefixes": "", 5 | "missing-prefixes": "" 6 | }, 7 | "flash-detection": { 8 | "swf-file-detected": "", 9 | "swfobject-lib-detected": "" 10 | } 11 | }, 12 | "interactions": { 13 | "alert": { 14 | "alert-detected": "" 15 | }, 16 | "touchscreen-target": { 17 | "too-small-touchscreen-target": "" 18 | } 19 | }, 20 | "performance": { 21 | "compression": { 22 | "resources-could-be-compressed": "" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /lib/logs/logs.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Current State

3 |

clients connected: <%= currentState.clients %>

4 |

validations running: <%= currentState.validations %>

5 |
6 |
7 |

History - <%= history.startDate %>

8 |

clients connections: <%= history.clients %>

9 |

validations runs: <%= history.validations %>

10 |
-------------------------------------------------------------------------------- /lib/profiles/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "width" : 320 3 | , "height" : 550 4 | } 5 | -------------------------------------------------------------------------------- /lib/profiles/smartphone2.json: -------------------------------------------------------------------------------- 1 | { 2 | "width" : 320 3 | , "height" : 480 4 | } 5 | -------------------------------------------------------------------------------- /lib/profiles/tablet.json: -------------------------------------------------------------------------------- 1 | { 2 | "width" : 853 3 | , "height" : 533 4 | } 5 | -------------------------------------------------------------------------------- /lib/reports/compatibility/css-prefixes/mismatching-prefixes.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Inconsistent usage of prefixed CSS properties.

5 |

Some of the CSS style sheets use prefixed properties, i.e. properties that may have not been standardized yet and that are specific to a given browser.

6 |

While using prefixed properties make it possible for a page to use leading edge advances in CSS, it is important that they be used in a way that does not affect compatibility with other browsers. Some prefixed properties have inconsistent values across prefixed names.

7 | 8 | 9 | 10 | <% var urls = Object.keys(mismatchingPrefixes); %> 11 | <% for(var i=0; i < urls.length; i++) {%> 12 | 13 | <% for(var j=0; j < mismatchingPrefixes[urls[i]].length; j++) {%> 14 | <% var mismatchingPrefix = mismatchingPrefixes[urls[i]][j]; %> 15 | 16 | 17 | 20 | 21 | <% } %> 22 | <% } %> 23 | 24 |
LocationMismatched declarations
<%=: urls[i] | ellipsizeUrl %>
line <%= mismatchingPrefix.position.line %>, col <%= mismatchingPrefix.position.column %>
<%= mismatchingPrefix.rule %>
<% for (var k=0 ; k < mismatchingPrefix.decls.length ; k++) { %> 18 | <%= mismatchingPrefix.decls[k] %>
19 | <%}%>
25 |

Broad compatibility with mobile browsers ensure as many users as possible will benefit from the web page. It also keeps the Web open!

26 |
27 |
28 | 29 |

For each identified CSS rule, ensure the various prefix equivalents have the same declaration.

30 |
31 | -------------------------------------------------------------------------------- /lib/reports/compatibility/css-prefixes/missing-prefixes.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Incomplete usage of prefixed CSS properties

5 |

Some of the CSS style sheets use prefixed properties, i.e. properties that may have not been standardized yet and that are specific to a given browser.

6 |

While using prefixed properties make it possible for a page to use leading edge advances in CSS, it is important that they be used in a way that does not affect compatibility with other browsers. Some prefixed properties are missing their equivalent for other browsers.

7 |
8 | <% var urls = Object.keys(missingPrefixes); %> 9 | <% for(var i=0; i < urls.length; i++) {%> 10 |

In <%=: urls[i] | ellipsizeUrl %>

11 |
12 | <% for(var j=0; j < missingPrefixes[urls[i]].length; j++) {%> 13 | <% var missingPrefix = missingPrefixes[urls[i]][j]; %> 14 |
On line <%= missingPrefix.position.line %>, col <%= missingPrefix.position.column %>
<%= missingPrefix.rule %>
15 |
Existing prefixed declarations:
<% for (var k=0 ; k < missingPrefix.decls.length ; k++) { %> 16 | <%= missingPrefix.decls[k] %>
17 | <%}%>
18 |
Missing:
<% for (var k=0 ; k < missingPrefix.missing.length ; k++) { %> 19 | <%= missingPrefix.missing[k]%>: <%= missingPrefix.value %>
20 | <%}%>
21 | <% } %> 22 |
23 | <% } %> 24 |
25 |

Broad compatibility with mobile browsers ensure as many users as possible will benefit from the web page. It also keeps the Web open!

26 |
27 |
28 | 29 |

For each identified CSS rule, add the equivalent prefix(es) for other browsers.

30 |
31 | -------------------------------------------------------------------------------- /lib/reports/compatibility/flash-detection/swf-file-detected.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Flash application detected

5 |

An Adobe Flash applet is loaded via the embed tag.

6 |

Adobe Flash will not run on many mobile devices.

7 |
8 |
9 | 10 |

Nowadays, technologies such as HTML5, SVG and the canvas API provide well-supported replacements for most Flash features.

11 |
12 | -------------------------------------------------------------------------------- /lib/reports/compatibility/flash-detection/swfobject-lib-detected.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Flash application detected

5 |

Usage of Adobe Flash has been detected on the page, through the swfobject Javascript library.

6 |

Adobe Flash will not run on many mobile devices.

7 |
8 |
9 | 10 |

Nowadays, technologies such as HTML5, SVG and the canvas API provide well-supported replacements for most Flash features.

11 |
-------------------------------------------------------------------------------- /lib/reports/format.js: -------------------------------------------------------------------------------- 1 | var jsdom = require("jsdom"); 2 | var template = '
\ 3 |
\ 4 |
\ 5 | \ 6 |
\ 7 |

HOW TO FIX IT:

\ 8 |
\ 9 |
\ 10 |
'; 11 | 12 | 13 | exports.format = function (markup, type, category, cb) { 14 | jsdom.env({html: template, done: function(errors, window) { 15 | var div = window.document.querySelector("body"); 16 | var report = window.document.createElement("div"); 17 | if (type === "error") { 18 | div.querySelector("span.octicon-issue-opened").setAttribute("class", "octicon octicon-stop"); 19 | } else if (type === "info") { 20 | div.querySelector("span.octicon-issue-opened").setAttribute("class", "octicon octicon-info"); 21 | } 22 | report.innerHTML = markup; 23 | var h2 = div.querySelector("h2.title-issue"); 24 | // Making tips collapsable 25 | if (category === "tip") { 26 | var issueIcon = div.querySelector("div#issue-status span"); 27 | issueIcon.parentNode.removeChild(issueIcon); 28 | var tipToggle = window.document.createElement("a"); 29 | tipToggle.setAttribute("data-toggle", "collapse"); 30 | tipToggle.setAttribute("href", "#tipbody"); 31 | h2.insertBefore(tipToggle, h2.firstChild); 32 | h2 = tipToggle; 33 | } 34 | div.querySelector(".category").textContent = category; 35 | var h2Children = report.querySelector("h2").childNodes; 36 | for (var i = 0; i < h2Children.length; i++) { 37 | h2.insertBefore(h2Children[i].cloneNode(true), h2.firstChild); 38 | } 39 | var content = window.document.createElement("div"); 40 | if (category === "tip") { 41 | content.setAttribute("id", "tipbody"); 42 | } 43 | var contentChildren = report.querySelectorAll(".issue > *:not(h2)"); 44 | for (var i = 0; i < contentChildren.length; i++) { 45 | content.appendChild(contentChildren[i].cloneNode(true)); 46 | } 47 | var fixit = div.querySelector(".fixit"); 48 | fixit.parentNode.insertBefore(content, fixit); 49 | var fix = report.querySelector(".fix"); 50 | if (fix === null) { 51 | fixit.parentNode.removeChild(fixit); 52 | } else { 53 | var children = fix.childNodes; 54 | for (var j = 0 ; j < children.length; j++) { 55 | fixit.appendChild(children[j].cloneNode(true)) 56 | } 57 | } 58 | var collapsable = div.querySelector(".collapsable"); 59 | if (collapsable) { 60 | var sortable = collapsable.classList && collapsable.classList.contains("sortable"); 61 | var isTable = collapsable.tagName === "table"; 62 | collapsable.setAttribute("class", "panel-collapse collapse" + (isTable ? " table" : "") + (sortable ? " sortable" : "")); 63 | var toggle = window.document.createElement("a"); 64 | toggle.setAttribute("data-toggle", "collapse"); 65 | toggle.setAttribute("href", "#" + collapsable.id); 66 | toggle.textContent = collapsable.title; 67 | collapsable.parentNode.insertBefore(toggle, collapsable); 68 | } 69 | var popovers = div.querySelectorAll(".popovers"); 70 | for (var p = 0 ; p < popovers.length; p++) { 71 | var pop = popovers[p]; 72 | var span = window.document.createElement("span"); 73 | span.setAttribute("class","info btn btn-xs btn-default glyphicon glyphicon-info-sign"); 74 | span.setAttribute("data-body", "container"); 75 | span.setAttribute("data-toggle", "popover"); 76 | span.setAttribute("data-placement", "bottom"); 77 | span.setAttribute("data-html", "true"); 78 | span.setAttribute("data-content", pop.outerHTML); 79 | span.setAttribute("title", pop.title); 80 | } 81 | if (popovers.length) { 82 | var script = window.document.createElement("script"); 83 | script.textContent = "$('span.info').popover();"; 84 | div.appendChild(script); 85 | } 86 | cb(div.innerHTML); 87 | }}); 88 | }; 89 | -------------------------------------------------------------------------------- /lib/reports/integration/manifest/httperror.ejs: -------------------------------------------------------------------------------- 1 |
2 |

App manifest could not be retrieved

3 |

The declared app manifest at <%= manifest %> could not be retrieved — it failed with HTTP error <%= httperror %>.

4 |
5 |
6 |

Check that the address for the manifest <%= manifest %> is correct, and make sure it has been properly uploaded to the server.

7 |
8 | -------------------------------------------------------------------------------- /lib/reports/integration/manifest/httperror_property.ejs: -------------------------------------------------------------------------------- 1 |
2 |

App manifest could not be retrieved

3 |

The declared app manifest at <%= manifest %> references the address 2 |

App manifest is not well-formed JSON

3 |

The declared app manifest at <%= manifest %> cannot be parsed as JSON — it fails with the error <%= error.message %>. Browsers will not be able to use it to install the app.

4 |
5 |
6 |

Make sure that the manifest file is well-formed JSON; watch out in particular that JSON only accepts double-quotes ("), that these are required around property names, and JSON does not accept trailing commas.

7 |
8 | -------------------------------------------------------------------------------- /lib/reports/integration/manifest/jsonserror.ejs: -------------------------------------------------------------------------------- 1 |
2 |

App manifest is not valid

3 |

The declared app manifest at <%= manifest %> does not follow the requirements set in the specification:

4 |
    5 | <% for(var i=0; i < errors.length; i++) {%> 6 |
  • <%= errors[i].stack %>
  • 7 | <% } %> 8 |
9 |

It thus risks not being interpreted correctly by browsers.

10 |
11 |
12 |

Make sure that the manifest file uses the properties is well-formed JSON; watch out in particular that JSON only accepts double-quotes ("), that these are required around property names, and JSON does not accept trailing commas.

13 |
14 | -------------------------------------------------------------------------------- /lib/reports/integration/manifest/multiple-manifests.ejs: -------------------------------------------------------------------------------- 1 |
2 |

More than one app manifests are declared

3 |

More than one (<%=links.length + 1%>) app manifests are declared in the HTML.

4 |

Only the first manifest is taken into account; the other declarations should be removed as they clutter the code.

5 |
6 |
7 | Keep only the first <link rel=manifest> declaration. 8 |
9 | -------------------------------------------------------------------------------- /lib/reports/integration/manifest/networkerror_property.ejs: -------------------------------------------------------------------------------- 1 | 8 |
9 | 10 | Remove alert(), prompt() or confirm() invokations from the JavaScript code. 11 |
12 | -------------------------------------------------------------------------------- /lib/reports/interactions/touchscreen-target/too-small-touchscreen-target.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Small touch targets

5 |

<%= size.width.length %> activable targets have been detected that are smaller than 48px wide or 48px high.

6 | 7 | 8 | 9 | <%for(var index in size.width){%> 10 | 11 | 12 | <%}%> 13 | 14 |
CodeTagSize
<%=outerHtml[index]%><%=tag[index]%><%=size.width[index]%>x<%=size.height[index]%>
15 |

Most mobile devices are prominently touch-based, which means they need larger activable targets than mouse-based devices. It is recommended that activable targets (such as links and form fields) be at least 48x48 pixels large to make them easy to hit with a finger.

16 |
17 |
18 | 19 | Style activable targets so that their width and height are larger than 48px. 20 |
21 | -------------------------------------------------------------------------------- /lib/reports/performance/compression/resources-could-be-compressed.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

<%=number%> resources could be compressed

5 |

<%=number%> resources loaded to display the page could be transmitted more quickly (saving <%=: saving | byteSize %>) if they were compressed:

6 | 7 | 8 | 9 | <% for (var i = 0; i 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% } %> 18 | 19 |
URLUncompressedCompressedSaving%
<%=: compressable[i].url | ellipsizeUrl %><%=: compressable[i].origSize | byteSize %><%=: (compressable[i].origSize - compressable[i].diff) | byteSize %><%=: compressable[i].diff | byteSize %><%= Math.floor(100 * compressable[i].diff / compressable[i].origSize) %>%
20 |

Reducing the overall amount of data needed to display the page makes it load faster, a critical component of the user experience on mobile browsers.

21 |
22 |
26 | -------------------------------------------------------------------------------- /lib/reports/performance/exif/images-could-be-unexified.ejs: -------------------------------------------------------------------------------- 1 |
2 |

<%=number%> images could have metadata removed

3 |

<%=number%> images displayed on the page could be transmitted more quickly (saving <%= Math.floor(saving / 1024) %> kB) if their embedded metadata were removed:

4 | 5 | 6 | 7 | <% for (var i = 0; i 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | <% } %> 16 | 17 |
URLWith metadataWithout metadataSaving%
<%=: minifiable[i].url | ellipsizeUrl %><%=: minifiable[i].origSize | number %>B<%=: (minifiable[i].origSize - minifiable[i].diff) | number %>B<%=: minifiable[i].diff | number %>B<%= Math.floor(100 * minifiable[i].diff / minifiable[i].origSize) %>%
18 |

Reducing the overall amount of data needed to display the page makes it load faster, a critical component of the user experience on mobile browsers.

19 |
20 |
21 |

Remove EXIF data from your images, using for instance the jhead utility in your production workflow.

22 |
23 | -------------------------------------------------------------------------------- /lib/reports/performance/http-errors/favicon.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Favicon unnecessarily checked

5 |

The page does not have a favicon at <%=url%> but the browser had to make a request to determine it.

6 |

Favicon helps users quickly identify a particular page in a list of opened windows; unless told otherwise, browsers always make requests to /favicon.ico to obtain these favicons. When no favicon is present, this generates unnecessary network traffic.

7 |
8 |
9 | 10 |

Set up a favicon on the domain, or specific to the page; otherwise, use <link rel='icon' href='data:;base64,iVBORw0KGgo='> in the head of the page to prevent the browser from doing the additional request. See Understanding the Favicon for in-depth information on browser and platform support.

11 |
-------------------------------------------------------------------------------- /lib/reports/performance/http-errors/http-errors-detected.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

<%=number%> HTTP errors encountered during page load

5 |

When loading the resources required to display the page from the network, <%=number%> HTTP errors were triggered.

6 | 7 | 8 | 9 | <% for(var i=0; i < errors.length; i++) {%> 10 | 11 | 12 | 13 | 14 | 15 | <% } %> 16 | 17 |
URLHTTP StatusHTTP Message
<%=: errors[i].url | ellipsizeUrl %><%= errors[i].status %><%= errors[i].statusText %>
18 |

HTTP errors imply that the target resource was not loaded, which can impact the proper functioning of the page and implies unneeded network traffic.

19 |
20 |
21 | 22 |

Check the listed errors to determine their origin and fix them or remove the resources from the page.

23 |
24 | -------------------------------------------------------------------------------- /lib/reports/performance/number-requests/info-number-requests.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Loading the page requires <%=number%> network requests

5 |

The following resources were loaded from the network:

6 | 7 | 8 | 9 | <% for(var i=0; i < entries.length; i++) {%> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <% } %> 18 | 19 |
URLHTTP StatusTypeSizeTime
<%=: entries[i].url | ellipsizeUrl %><%= entries[i].status %><%= entries[i].mimeType %><%=: entries[i].bodySize | byteSize %><%= entries[i].time|number %> ms
20 |
21 |
22 | 23 |

You can reduce the number of network requests by combining (concatenating) files such as JavaScript and CSS files. It's also possible to combine several image files into a single larger image and use CSS to display parts of that image as needed — a technique known as CSS sprites. See The Mystery Of CSS Sprites for a detailed explanation and many useful links.

24 |
25 | -------------------------------------------------------------------------------- /lib/reports/performance/redirects/redirects-encountered.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

<%=number%> redirects encountered during page load

5 |

When loading the resources required to display the page from the network, <%=number%> HTTP redirects were triggered.

6 |

Each HTTP redirects require an additional round trip on the network, which slows down the page load without any benefit to the user. 7 | 8 | 9 | 10 | <% for(var i=0; i 11 | 12 | 13 | 14 | 15 | 16 | <% } %> 17 | 18 | 19 | 20 | 21 | 22 | 23 |
FromToWasted network trafficAdditional latency
<%=: redirects[i].from | ellipsizeUrl %><%=: redirects[i].to | ellipsizeUrl %><%=: redirects[i].wastedBW | byteSize %><%=: redirects[i].latency | number %>ms
Total<%=: totalWastedBW | byteSize %><%=: totalLatency | number %>ms
24 |

25 |
26 | 27 |

Use the final destination URL for the resources.

28 |
29 | -------------------------------------------------------------------------------- /lib/reports/responsive/doc-width/doc-width-too-large.ejs: -------------------------------------------------------------------------------- 1 |
2 |

The width of the page is larger than an average mobile browser size

3 |

The width of the page is larger than an average mobile browser size - this means users will have to scroll horizontally and vertically, or zoom in and out.

4 |
5 | -------------------------------------------------------------------------------- /lib/reports/responsive/fonts-size/too-small-font-size.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Too small font size detected

5 |

Elements detected with font-size lower than 16px:

6 |
    7 | <%for(index in size){%> 8 |
  • <%=content[index]%> (<%=tag[index]%> element) display with small font size. (<%=size[index]%> pixels CSS)
  • 9 | <%}%> 10 |
11 |

On a mobile device, it's recommended to not put font-size lower than 16px because the user experience will be deteriorated with small font size. It could force user to zoom on their mobile device.

12 |
13 |
14 | 15 |

It's recommended to put font-size greater than 16px.

16 |
-------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/content-viewport-missed.ejs: -------------------------------------------------------------------------------- 1 |
2 |

The meta viewport does not declare a width nor an initial-scale property

3 |

The meta viewport does not declare a width, nor an initial-scale, and thus will not adapt the Web page to mobile screens.

4 |
5 |
6 |

Declare a mobile viewport using a meta viewport tag in the head of the HTML markup. Learn more about the mobile viewport.

7 |
8 | -------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/hardcoded-viewport-width.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Hardcoded viewport width

3 |

The meta viewport uses an hardcoded value for the width of the viewport.

4 |

A numeric hardcoded value for the meta viewport declaration makes the page less responsive to the actual screen used; if the width is larger than the screen width, the page will be hard to read and will not benefit from some optimizations (e.g. removal of the 300 ms delay in clicks); if the page is significantly more narrow than the screen (as is likely on tablets), the page will not benefit from the whole screen real estate.

5 |
6 |
7 |

Use device-width as the value of the width parameter in the meta viewport declaration. Learn more about the mobile viewport

8 |
-------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/invalid-viewport-value.ejs: -------------------------------------------------------------------------------- 1 |
2 |

Invalid value for <%=property%> in meta viewport

3 |

The property <%=property%> in the meta viewport declaration has an invalid value: <%=value%>.

4 |

Because <%=value%> is not a recognized value for <%=property%>, it will be ignored by the browser.

5 |
6 |
7 |

Use a valid value for <%=property%>, one of: <%=validValues%>. Learn more about the mobile viewport

8 |
-------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/no-viewport-declared.ejs: -------------------------------------------------------------------------------- 1 |
2 |

No mobile viewport declared

3 |

No mobile viewport declared. The meta viewport tag helps adapt the way Web pages are displayed in mobile browsers.

4 |

Without meta viewport declared, mobile browsers will render a Web page as they would on a ~1000px screen; in most cases, this will force users to zoom before they can interact with the page, and then they will have to scroll awkwardly to navigate on the page.

5 |
6 |
7 |

Declare a mobile viewport using a meta viewport tag in the head of the HTML markup. Learn more about the mobile viewport

8 |
-------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/non-standard-viewport-parameter-declared.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Non-standard parameter in viewport declaration

5 |

The following properties are used in a meta viewport declaration but are not defined in the reference specification: 6 | <% for(var i=0; i < nonstandardProperties.length; i++) {%> 7 | <%= nonstandardProperties[i] %> 8 | <% if (i < nonstandardProperties.length - 1) {%>, <% } %> 9 | <% } %> 10 |

11 |

Relying on their usage may create inconsistent user experiences across browsers.

12 |
13 |
14 | 15 |

Avoid using non-standard properties in the meta viewport declaration. Learn more about the mobile viewport.

16 |
17 | 18 | -------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/several-viewports-declared.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

More than one viewports are declared

5 |

More than one viewports are declared in the HTML. Please check your HTML file keep a single meta viewport

6 |
7 |

Only the last meta viewport declaration is taken into account; the other declarations should be removed as they clutter the code and may have an impact on performance.

8 |
9 |
10 | 11 |

Keep only the last meta viewport declaration. Learn more about the mobile viewport.

12 |
-------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/unknow-viewport-parameter-declared.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Unknown parameter in viewport declaration

5 |

The following properties are used in a meta viewport declaration but cannot be recognized as valid values: 6 | <% for(var i=0; i < unrecognizedProperties.length; i++) {%> 7 | <%= unrecognizedProperties[i] %> 8 | <% if (i < unrecognizedProperties.length - 1) {%>, <% } %> 9 | <% } %> 10 |

11 |

They may be due to a coding error, or linked to proprietary extensions of the meta viewport.

12 |
13 |
14 | 15 |

Check there is no type in the meta viewport declaration and avoid using non-standard properties in the meta viewport declaration. Learn more about the mobile viewport.

16 |
-------------------------------------------------------------------------------- /lib/reports/responsive/meta-viewport/users-are-prevented-to-zoom.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Users are prevented to zoom

5 |

The meta viewport declaration prevents users to zoom in on the page.

6 |

Except in some specific cases (e.g. games), preventing users to zoom in a page is discouraged; users may want to see details in an image, or may have trouble reading at the current zoom level.

7 |
8 |
9 | 10 |

Remove the constraints on user-scalable or minimum-/maximum-scale. Learn more about the mobile viewport.

11 |
-------------------------------------------------------------------------------- /lib/tips/accessible.html: -------------------------------------------------------------------------------- 1 |

Mobile friendly AND accessible

2 |

The W3C Web Accessibility Initiative has started documenting techniques to make sure that your Web app works well with assistive technologies on mobile devices: make sure to check them out to ensure the broadest possible adoption of your app!

3 | -------------------------------------------------------------------------------- /lib/tips/appmanifest.html: -------------------------------------------------------------------------------- 1 |

Make your Web app stick with an app manifest

2 |

The Manifest for Web applications specification defines a JSON format that lets you describe your Web app with a name, a preferred start page, icons, and a specific display mode (e.g. fullscreen).

3 |

Mobile browsers are starting to use these manifests to facilitate the addition of Web apps to users’ home screens, a great way to keep your users just one-click away from your app!

4 | -------------------------------------------------------------------------------- /lib/tips/deviceapis.html: -------------------------------------------------------------------------------- 1 |

There is an API for that

2 |

Think you need a native app to interact with mobile devices specific hardware? Think again!

3 |

Mobile browsers already let you access the GPS, accelerometer, compass, camera, microphone and battery gauge of the device via JavaScript APIs, and work on adding support for more sensors is under way in W3C — you can stay abreast of all the activity in this space via the quarterly updated overview of standards for Web apps on mobile.

4 | -------------------------------------------------------------------------------- /lib/tips/dontpushaway.html: -------------------------------------------------------------------------------- 1 |

Don’t push away your users!

2 |

Once users have landed on to your mobile Web app, don’t start by pushing them away to a native app; if your native app provides additional features, mention that to the user once they got a chance to try and find what they wanted on the Web app first!

3 | -------------------------------------------------------------------------------- /lib/tips/offline.html: -------------------------------------------------------------------------------- 1 |

Make your Web app work offline

2 |

Mobile devices are great because they’re always connected… except when they’re not, or when the connection is poor.

3 |

To keep your users engaged even in adverse networking conditions, look into making your Web app work offline: the widely deployed HTML5 application cache and its more experimental but much better replacement ServiceWorker let you make your Web app run seamlessly even in the deepest tunnels.

4 | -------------------------------------------------------------------------------- /lib/tips/pushnotifications.html: -------------------------------------------------------------------------------- 1 |

Push notifications in your Web app

2 |

Mobile Web apps can now start benefitting from push notifications, making it possible for your users to subscribe to the latest news and events from your Web app, even after they closed their browsers.

3 |

Well-designed push notifications have been shown to significantly grow user’s engagement, so give them a try!

4 | -------------------------------------------------------------------------------- /lib/tips/responsiveimages.html: -------------------------------------------------------------------------------- 1 |

Make your images responsive

2 |

If you’re using responsive design for your mobile Web app, you may want to take advantage of the ability to declare your images at various resolution (both in markup and style sheets), so that mobile browsers will load the most adapted image given their screen resolution.

3 |

The <picture> element event lets you declare separate images based on more advanced media queries.

4 | -------------------------------------------------------------------------------- /lib/tips/serviceworker.html: -------------------------------------------------------------------------------- 1 |

ServiceWorker: soon in a mobile browser near you

2 |

ServiceWorker is a technology under active development in W3C and under implementation in various browsers; it promises a very flexible framework to handle offline Web applications, and in the longer term, a model to have Web apps work in the background, even with no active browser.

3 |

Check out the specification, and if you feel like it, come and join us in evaluating it by building example apps.

4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile-checker", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/w3c/Mobile-Checker.git" 8 | }, 9 | "dependencies": { 10 | "binary-reader":"0.1.2", 11 | "compression": "1.0.11", 12 | "css": "2.1.0", 13 | "easyimage": "1.0.2", 14 | "effective-domain-name-parser": "0.0.1", 15 | "ejs": "1.0.0", 16 | "express": "4.4.1", 17 | "headless": "0.2.1", 18 | "image-headers":"0.3.3", 19 | "insafe": "^0.3.0", 20 | "intl": "^0.1.4", 21 | "ip": "0.3.1", 22 | "media-type": "0.1.0", 23 | "metaviewport-parser": "^0.0.1", 24 | "mkdirp": "0.5.0", 25 | "mobile-web-browser-emulator": "0.0.6", 26 | "node-uuid": "1.4.8", 27 | "q": "1.0.1", 28 | "request": "^2.83.0", 29 | "jsonschema": "^1.0.1", 30 | "socket.io": "1.0.4" 31 | }, 32 | "devDependencies": { 33 | "coveralls": "^2.11.2", 34 | "expect.js": "^0.3.1", 35 | "istanbul": "^0.4.5", 36 | "jsdom": "4.0.4", 37 | "mocha": "^2.2.5" 38 | }, 39 | "scripts": { 40 | "coverage": "istanbul cover _mocha", 41 | "coveralls": "npm run coverage && cat ./coverage/lcov.info | coveralls", 42 | "start": "node app.js", 43 | "test": "mocha --timeout 20000 --globals Intl,IntlPolyfill,ErrorHandler" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/css/bootstrap-sortable.css: -------------------------------------------------------------------------------- 1 | table.sortable span.sign { 2 | display: block; 3 | position: absolute; 4 | top: 50%; 5 | right: 5px; 6 | font-size: 12px; 7 | margin-top: -10px; 8 | color: #bfbfc1; 9 | } 10 | 11 | table.sortable span.arrow, span.reversed { 12 | border-style: solid; 13 | border-width: 5px; 14 | font-size: 0; 15 | border-color: #ccc transparent transparent transparent; 16 | line-height: 0; 17 | height: 0; 18 | width: 0; 19 | margin-top: -2px; 20 | } 21 | 22 | table.sortable span.arrow.up { 23 | border-color: transparent transparent #ccc transparent; 24 | margin-top: -7px; 25 | } 26 | 27 | table.sortable span.reversed { 28 | border-color: transparent transparent #ccc transparent; 29 | margin-top: -7px; 30 | } 31 | 32 | table.sortable span.reversed.up { 33 | border-color: #ccc transparent transparent transparent; 34 | margin-top: -2px; 35 | } 36 | 37 | 38 | 39 | table.sortable span.az:before { 40 | content: "a .. z"; 41 | } 42 | 43 | table.sortable span.az.up:before { 44 | content: "z .. a"; 45 | } 46 | 47 | table.sortable span.AZ:before { 48 | content: "A .. Z"; 49 | } 50 | 51 | table.sortable span.AZ.up:before { 52 | content: "Z .. A"; 53 | } 54 | 55 | table.sortable span._19:before { 56 | content: "1 .. 9"; 57 | } 58 | 59 | table.sortable span._19.up:before { 60 | content: "9 .. 1"; 61 | } 62 | 63 | table.sortable span.month:before { 64 | content: "jan .. dec"; 65 | } 66 | 67 | table.sortable span.month.up:before { 68 | content: "dec .. jan"; 69 | } 70 | 71 | table.sortable thead th:not([data-defaultsort=disabled]) { 72 | cursor: pointer; 73 | position: relative; 74 | top: 0; 75 | left: 0; 76 | } 77 | 78 | table.sortable thead th:hover:not([data-defaultsort=disabled]) { 79 | background: #efefef; 80 | } 81 | 82 | table.sortable thead th div.mozilla { 83 | position: relative; 84 | } 85 | -------------------------------------------------------------------------------- /public/css/logs.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | 4 | .navbar { 5 | margin-top: 25px; 6 | font-family: 'Roboto'; 7 | } 8 | 9 | section { 10 | color: #666; 11 | } -------------------------------------------------------------------------------- /public/css/report.css: -------------------------------------------------------------------------------- 1 | #report { 2 | padding: 0px; 3 | background-color: #ecf0f1; 4 | } 5 | 6 | .navbar-default { 7 | background-image: none; 8 | background-color: transparent; 9 | box-shadow: none; 10 | border-radius: 0px; 11 | border: 0px; 12 | padding-top: 15px; 13 | text-align: center; 14 | color: #384047; 15 | font-size: 16px; 16 | font-weight: 300; 17 | } 18 | 19 | .navbar-default a { 20 | color: #384047; 21 | } 22 | 23 | .navbar-active { 24 | background-color: #334d5c; 25 | transition:0.6s; 26 | color: white; 27 | } 28 | 29 | .navbar-active a { 30 | color: white; 31 | transition:0.6s; 32 | } 33 | 34 | #report > #console-title { 35 | padding-left: 20px; 36 | margin-top: 100px; 37 | font-size: 18px; 38 | font-weight: 700; 39 | color: #8D9AA5; 40 | } 41 | #report > #console { 42 | background-color: #3C3C3C; 43 | height: 400px; 44 | margin-left: 20px; 45 | overflow:auto; 46 | padding: 10px; 47 | color: #FCFCFB; 48 | } 49 | #report > .report-title { 50 | padding: 0px; 51 | margin-top: 20px; 52 | font-size: 20px; 53 | font-weight: 700; 54 | color: #8D9AA5; 55 | text-align: center; 56 | font-family: Bitter; 57 | } 58 | #report > #issues-feed .issue { 59 | padding-left: 5px; 60 | padding-right: 5px; 61 | padding-bottom: 5px; 62 | padding-top: 15px; 63 | color: black; 64 | border-radius: 0px; 65 | overflow-x: auto; 66 | border-bottom: 5px solid #ecf0f1; 67 | --box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); 68 | padding: 0px; 69 | border-radius: 2px; 70 | background-color: #ecf0f1; 71 | } 72 | 73 | dl.report { background-color: #eee;} 74 | 75 | table { 76 | width: 100%; 77 | overflow: auto; 78 | background-color: #eee;} 79 | 80 | #side-nav { 81 | height: 50px; 82 | font-size: 14px; 83 | text-align: left; 84 | margin-top: 0px; 85 | padding-top: 20px; 86 | } 87 | 88 | #side-nav a { 89 | color: #8D9AA5; 90 | } 91 | 92 | #side-nav span { 93 | padding-top: 10px; 94 | padding-bottom: 10px; 95 | font-size: 16px; 96 | } 97 | #side-nav .return-button { 98 | padding: 0px; 99 | } 100 | #side-nav .return-button a { 101 | padding: 10px; 102 | background-color: white; 103 | border-radius: 2px; 104 | color: #384047; 105 | border-bottom: 2px solid #eee; 106 | } 107 | 108 | #side-nav .return-button a:hover { 109 | transition:0.3s; 110 | padding: 10px; 111 | background-color: #eee; 112 | } 113 | 114 | #report > #issues-feed { 115 | margin-top: 10px; 116 | } 117 | 118 | .issue-category { 119 | margin-top: 5px; 120 | color: #999; 121 | } 122 | 123 | #report > #issues-feed .issue > div > .title-issue { 124 | margin-top: 0px; 125 | margin-bottom: 5px; 126 | font-weight: 700; 127 | font-size: 14px; 128 | font-family: Open Sans; 129 | color: #334d5c; 130 | text-align: left; 131 | text-transform: uppercase; 132 | } 133 | 134 | #report > #issues-feed .issue > .content-issue { 135 | background-color: white; 136 | padding: 10px; 137 | border-radius: 2px; 138 | } 139 | 140 | #report > #issues-feed .issue > .content-issue > p { 141 | font-weight: 400; 142 | color: #666; 143 | font-size: 1rem; 144 | } 145 | #report > #issues-feed .issue > .content-issue > h3 { 146 | font-weight: 300; 147 | font-size: 1.2rem; 148 | color: #384047; 149 | } 150 | 151 | #system-info { 152 | padding: 0px; 153 | } 154 | 155 | #report .alert { 156 | background-color: #ecf0f1; 157 | padding-bottom: 20px; 158 | margin-bottom: 0px; 159 | border: 0px; 160 | 161 | } 162 | 163 | #report .alert h2 { 164 | font-size:1.2em; 165 | margin: 5px 0; 166 | } 167 | 168 | #system-info p{ 169 | text-align: left; 170 | font-size: 14px; 171 | font-weight: 400; 172 | color: #e74c3c; 173 | padding: 10px; 174 | background-color: white; 175 | border-radius: 2px; 176 | border: 1px solid rgba(0, 0, 0, 0.2); 177 | } 178 | 179 | #error-issue-feed, #warning-issue-feed, #info-issue-feed{ 180 | padding: 0px; 181 | margin-top: 20px; 182 | background-color: white; 183 | border-radius: 2px; 184 | } 185 | 186 | #tip-issue-feed { 187 | padding: 0px; 188 | margin-top: 0px; 189 | } 190 | 191 | #error-issue-feed .title-issue{ 192 | } 193 | 194 | #warning-issue-feed .title-issue{ 195 | } 196 | 197 | #info-issue-feed .title-issue{ 198 | } 199 | 200 | #error-issue-feed #issue-status span { 201 | font-size: 40px; 202 | color: #df5a49; 203 | } 204 | 205 | #warning-issue-feed #issue-status span { 206 | font-size: 40px; 207 | color: #efc94c; 208 | } 209 | 210 | #info-issue-feed #issue-status span { 211 | font-size: 40px; 212 | color: #3498db; 213 | } 214 | 215 | #tip-issue-feed .issue { 216 | background-color: transparent; 217 | } 218 | 219 | #tip-issue-feed .issue .title-issue { 220 | background-color: white; 221 | } 222 | 223 | #issue-status { 224 | 225 | } 226 | #issue-status span { 227 | margin-top: 10px; 228 | font-size: 40px; 229 | } 230 | 231 | .fixit span { 232 | color: #44ad99; 233 | font-size: 20px; 234 | } 235 | 236 | .alert-warning { 237 | color: #9F890C; 238 | } 239 | 240 | .alert-info { 241 | color: #3498db; 242 | } 243 | 244 | #report > #overview { 245 | padding: 0px; 246 | margin-top: 30px; 247 | } 248 | #report > #overview > #smartphone { 249 | height: 700px; 250 | background-repeat:no-repeat; 251 | margin: auto; 252 | } 253 | #report > #overview > #smartphone.sm { 254 | background-image: url(../img/smartphone.svg); 255 | } 256 | #report > #overview > #smartphone.sm2 { 257 | background-image: url(../img/smartphone2.svg); 258 | } 259 | #report > #overview > #smartphone.tab { 260 | background-image: url(../img/tablet.svg); 261 | background-size:295px auto; 262 | } 263 | #report > #overview > .sm #screenshot, #report > #overview > .sm2 #screenshot { 264 | margin-left: 28px; 265 | margin-top:82px; 266 | } 267 | #report > #overview > .sm2 #screenshot { 268 | margin-top:72px; 269 | } 270 | #report > #overview > .tab #screenshot { 271 | margin-left: 15px; 272 | margin-top:16px; 273 | } 274 | 275 | /* ANIMATION */ 276 | #sm, #sm2, #tab, #home, #report { 277 | transition: -webkit-transform 1s; 278 | transition: transform 1s; 279 | } 280 | .screenshot { 281 | -webkit-transform-origin: 0 0 0; 282 | transform-origin: 0 0 0; 283 | } 284 | #report { 285 | position: absolute; 286 | top: 0; 287 | -webkit-transform: translate3d(100%,0,0); 288 | transform: translate3d(100%,0,0); 289 | } 290 | #report.report { 291 | -webkit-transform: translate3d(0,0,0); 292 | transform: translate3d(0,0,0); 293 | } 294 | #home.report { 295 | -webkit-transform: translate3d(-100%, 0,0); 296 | transform: translate3d(-100%, 0,0); 297 | } 298 | #inprogress { 299 | opacity:0.5; 300 | } 301 | #cogs { 302 | text-align: center; 303 | } 304 | #cog2 { margin-top: -12px;} 305 | #cog1 { margin-top: 12px;} 306 | #cog1.active, #cog2.active { 307 | -webkit-animation: 2s rotate linear infinite; 308 | animation: 2s rotate linear infinite; 309 | -webkit-transform-origin: 50% 50%; 310 | transform-origin: 50% 50%; 311 | } 312 | 313 | .fixit { 314 | padding: 0px; 315 | color: #666; 316 | background-color: white; 317 | margin-top: 10px; 318 | margin-bottom: 0px; 319 | } 320 | 321 | .fixit h2 { 322 | margin-top: 0px; 323 | margin-bottom: 5px; 324 | font-weight: 700; 325 | font-size: 14px; 326 | font-family: Open Sans; 327 | color: #999; 328 | text-align: left; 329 | text-transform: uppercase; 330 | } 331 | 332 | .range-issue { 333 | font-size: 20px; 334 | text-align: center; 335 | margin-top: 5px; 336 | } 337 | 338 | #cog2.active { 339 | -webkit-animation-direction: reverse; 340 | animation-direction: reverse; 341 | } 342 | @keyframes rotate { 343 | from { 344 | transform: rotate(0); 345 | } 346 | to { 347 | transform: rotate(360deg); 348 | } 349 | } 350 | 351 | @-webkit-keyframes rotate { 352 | from { 353 | -webkit-transform: rotate(0); 354 | } 355 | to { 356 | -webkit-transform: rotate(360deg); 357 | } 358 | } 359 | 360 | @media screen and (max-width: 990px) { 361 | 362 | table tbody { 363 | max-width: 100%; 364 | overflow: auto; 365 | } 366 | .return-button { 367 | visibility: hidden; 368 | } 369 | .navbar-default { 370 | padding-top: 0px; 371 | } 372 | #title-report { 373 | margin-top: 0px; 374 | } 375 | 376 | } 377 | 378 | td.number { text-align:right;} 379 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | html, body{ 2 | margin: 0px; 3 | padding: 0px; 4 | outline: 0px; 5 | height: 100%; 6 | font-family: Lato; 7 | font-size: 18px; 8 | color: #455164; 9 | background-color: #ecf0f1; 10 | } 11 | #home .navbar { 12 | padding-top: 20px; 13 | } 14 | 15 | #home #hero { 16 | text-align: center; 17 | padding-top: 0px; 18 | } 19 | 20 | #home #hero img { 21 | margin: auto; 22 | } 23 | 24 | #home #hero p { 25 | margin-top: 30px; 26 | font-size: 1.2rem; 27 | font-weight: 300; 28 | } 29 | 30 | main { 31 | padding-top: 50px; 32 | padding-bottom: 50px; 33 | } 34 | 35 | main #errors .alert-danger { 36 | margin: 0px; 37 | border: 0px; 38 | border-radius: 2px; 39 | font-size: 0.9rem; 40 | color: #df5a49; 41 | background-color: transparent; 42 | } 43 | main form { 44 | max-width: 1000px; 45 | margin: auto; 46 | } 47 | 48 | main form #url { 49 | width: 100%; 50 | height: 60px; 51 | padding-left: 10px; 52 | padding-right: 10px; 53 | border: none; 54 | border-radius: 2px; 55 | outline-width: 0px; 56 | box-shadow: none; 57 | font-family: Lato; 58 | font-size: 1rem; 59 | } 60 | 61 | main form #submit { 62 | width: 100%; 63 | height: 60px; 64 | border: 0px; 65 | border-radius: 2px; 66 | background-image: none; 67 | background-color: #89C4F4; 68 | outline-width: 0; 69 | } 70 | 71 | main form #submit span { 72 | color: #333c4e; 73 | font-size: 2rem; 74 | text-shadow: 2px 2px rgba(0,0,0,0.3); 75 | } 76 | 77 | main form fieldset .btn-group { 78 | width: 100%; 79 | border: 0px; 80 | border-radius: 2px; 81 | background-color: white; 82 | } 83 | 84 | main form fieldset .btn-group .btn { 85 | margin-left: 0px !important; 86 | border: 0px; 87 | border-radius: 2px; 88 | padding-top: 20px; 89 | padding-bottom: 20px; 90 | font-family: Lato; 91 | font-size: 1rem; 92 | color: #777; 93 | } 94 | 95 | main form fieldset .btn-group .btn:active { 96 | margin-left: 0px !important; 97 | background-color: rgba(0,0,0,0.1); 98 | box-shadow: none; 99 | } 100 | 101 | main form fieldset .btn-group .btn.active { 102 | margin-left: 0px !important; 103 | background-color: rgba(0,0,0,0.1); 104 | box-shadow: none; 105 | } 106 | 107 | main form fieldset .btn-group .btn img { 108 | margin-bottom: 10px; 109 | } 110 | 111 | #home #info { 112 | padding-top: 50px; 113 | padding-bottom: 50px; 114 | background-color: #333c4e; 115 | color: white; 116 | } 117 | 118 | #home #info h2 { 119 | font-size: 1.2rem; 120 | font-weight: 700; 121 | color: #89C4F4; 122 | } 123 | 124 | #home #info p { 125 | font-size: 1rem; 126 | font-weight: 400; 127 | } 128 | 129 | #home #info a { 130 | color: #89C4F4; 131 | } 132 | 133 | #home #donate { 134 | background-color: white; 135 | padding-top: 50px; 136 | padding-bottom: 50px; 137 | } 138 | 139 | #home #donate h2 { 140 | font-size: 1.2rem; 141 | font-weight: 700; 142 | color: #EE677E; 143 | } 144 | 145 | #home #donate img { 146 | margin-top: 20px; 147 | } 148 | 149 | #home #donate a.donate { 150 | border-radius: 2px; 151 | font-size: 1rem; 152 | padding: 10px; 153 | background-color: #EE677E; 154 | color: white; 155 | } 156 | 157 | footer { 158 | padding-top: 50px; 159 | padding-bottom: 50px; 160 | background-color: #333c4e; 161 | text-align: center; 162 | color: white; 163 | } 164 | 165 | footer #credits { 166 | } 167 | 168 | footer #credits span { 169 | font-size: 1.2rem; 170 | } 171 | 172 | footer a { 173 | color: #89C4F4; 174 | } 175 | 176 | footer #copyright { 177 | font-size: 0.8rem; 178 | margin-top: 20px; 179 | } 180 | 181 | @media screen and (max-width: 992px) { 182 | #home #hero { 183 | padding-top: 50px; 184 | } 185 | #home #submit { 186 | margin-top: 10px; 187 | } 188 | footer { 189 | text-align: left; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/img/cog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 27 | 28 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 66 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/img/developers-min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | W3C developers 4 | 45 | -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/img/favicon.ico -------------------------------------------------------------------------------- /public/img/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 25 | 29 | 33 | 34 | 38 | 42 | 43 | 44 | 66 | 68 | 69 | 71 | image/svg+xml 72 | 74 | 75 | 76 | 77 | 78 | 83 | 89 | 90 | 95 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /public/img/mobile-checker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/img/mobile-checker-icon.png -------------------------------------------------------------------------------- /public/img/mobile-checker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/img/mobilechecker-logo-w3c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/img/mobilechecker-logo-w3c.png -------------------------------------------------------------------------------- /public/img/mobilechecker-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/img/mobilechecker-logo.png -------------------------------------------------------------------------------- /public/img/opensource.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 43 | 47 | 48 | 50 | 51 | 53 | image/svg+xml 54 | 56 | 57 | 58 | 59 | 60 | 64 | 71 | 79 | open 105 | source 131 | initiative 157 | 163 | 173 | R 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /public/img/opensource_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/img/opensource_logo.png -------------------------------------------------------------------------------- /public/img/smartphone.svg: -------------------------------------------------------------------------------- 1 | 2 | Original icon made by Freepik from www.flaticon.comimage/svg+xml -------------------------------------------------------------------------------- /public/img/smartphone2.svg: -------------------------------------------------------------------------------- 1 | 2 | Original icon made by Freepik from www.flaticon.comimage/svg+xml -------------------------------------------------------------------------------- /public/img/tablet.svg: -------------------------------------------------------------------------------- 1 | 2 | Original icon made by Freepik from www.flaticon.comimage/svg+xml -------------------------------------------------------------------------------- /public/img/w3c-developers-test-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 21 | 22 | 23 | 24 | 25 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /public/img/w3c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/js/bootstrap-sortable.js: -------------------------------------------------------------------------------- 1 | /* TINY SORT modified according to this https://github.com/Sjeiti/TinySort/pull/51*/ 2 | (function (e, t) { function h(e) { return e && e.toLowerCase ? e.toLowerCase() : e } function p(e, t) { for (var r = 0, i = e.length; r < i; r++) if (e[r] == t) return !n; return n } var n = !1, r = null, i = parseFloat, s = Math.min, o = /(-?\d+\.?\d*)$/g, u = /(\d+\.?\d*)$/g, a = [], f = [], l = function (e) { return typeof e == "string" }, c = Array.prototype.indexOf || function (e) { var t = this.length, n = Number(arguments[1]) || 0; n = n < 0 ? Math.ceil(n) : Math.floor(n); if (n < 0) n += t; for (; n < t; n++) { if (n in this && this[n] === e) return n } return -1 }; e.tinysort = { id: "TinySort", version: "1.5.2", copyright: "Copyright (c) 2008-2013 Ron Valstar", uri: "http://tinysort.sjeiti.com/", licensed: { MIT: "http://www.opensource.org/licenses/mit-license.php", GPL: "http://www.gnu.org/licenses/gpl.html" }, plugin: function () { var e = function (e, t) { a.push(e); f.push(t) }; e.indexOf = c; return e }(), defaults: { order: "asc", attr: r, data: r, useVal: n, place: "start", returns: n, cases: n, forceStrings: n, ignoreDashes: n, sortFunction: r } }; e.fn.extend({ tinysort: function () { var d, v, m = this, g = [], y = [], b = [], w = [], E = 0, S, x = [], T = [], N = function (t) { e.each(a, function (e, n) { n.call(n, t) }) }, C = function (t, r) { var s = 0; if (E !== 0) E = 0; while (s === 0 && E < S) { var a = w[E], c = a.oSettings, p = c.ignoreDashes ? u : o; N(c); if (c.sortFunction) { s = c.sortFunction(t, r) } else if (c.order == "rand") { s = Math.random() < .5 ? 1 : -1 } else { var d = n, v = !c.cases ? h(t.s[E]) : t.s[E], m = !c.cases ? h(r.s[E]) : r.s[E]; v = v.replace(/^\s*/i, "").replace(/\s*$/i, ""); m = m.replace(/^\s*/i, "").replace(/\s*$/i, ""); if (!A.forceStrings) { var g = l(v) ? v && v.match(p) : n, y = l(m) ? m && m.match(p) : n; if (g && y) { var b = v.substr(0, v.length - g[0].length), x = m.substr(0, m.length - y[0].length); if (b == x) { d = !n; v = i(g[0]); m = i(y[0]) } } } s = a.iAsc * (v < m ? -1 : v > m ? 1 : 0) } e.each(f, function (e, t) { s = t.call(t, d, v, m, s) }); if (s === 0) E++ } return s }; for (d = 0, v = arguments.length; d < v; d++) { var k = arguments[d]; if (l(k)) { if (x.push(k) - 1 > T.length) T.length = x.length - 1 } else { if (T.push(k) > x.length) x.length = T.length } } if (x.length > T.length) T.length = x.length; S = x.length; if (S === 0) { S = x.length = 1; T.push({}) } for (d = 0, v = S; d < v; d++) { var L = x[d], A = e.extend({}, e.tinysort.defaults, T[d]), O = !(!L || L == ""), M = O && L[0] == ":"; w.push({ sFind: L, oSettings: A, bFind: O, bAttr: !(A.attr === r || A.attr == ""), bData: A.data !== r, bFilter: M, $Filter: M ? m.filter(L) : m, fnSort: A.sortFunction, iAsc: A.order == "asc" ? 1 : -1 }) } m.each(function (n, r) { var i = e(r), s = i.parent().get(0), o, u = []; for (j = 0; j < S; j++) { var a = w[j], f = a.bFind ? a.bFilter ? a.$Filter.filter(r) : i.find(a.sFind) : i; u.push(a.bData ? f.data(a.oSettings.data) : a.bAttr ? f.attr(a.oSettings.attr) : a.oSettings.useVal ? f.val() : f.text()); if (o === t) o = f } var l = c.call(b, s); if (l < 0) { l = b.push(s) - 1; y[l] = { s: [], n: [] } } if (o.length > 0) y[l].s.push({ s: u, e: i, n: n }); else y[l].n.push({ e: i, n: n }) }); e.each(y, function (e, t) { t.s.sort(C) }); e.each(y, function (t, r) { var i = r.s.length, o = [], u = i, a = [0, 0]; switch (A.place) { case "first": e.each(r.s, function (e, t) { u = s(u, t.n) }); break; case "org": e.each(r.s, function (e, t) { o.push(t.n) }); break; case "end": u = r.n.length; break; default: u = 0 } for (d = 0; d < i; d++) { var f = p(o, d) ? !n : d >= u && d < u + r.s.length, l = (f ? r.s : r.n)[a[f ? 0 : 1]].e; l.parent().append(l); if (f || !A.returns) g.push(l.get(0)); a[f ? 0 : 1]++ } }); m.length = 0; Array.prototype.push.apply(m, g); return m } }); e.fn.TinySort = e.fn.Tinysort = e.fn.tsort = e.fn.tinysort })(jQuery); 3 | 4 | (function ($) { 5 | 6 | var $document = $(document), 7 | signClass; 8 | 9 | $.bootstrapSortable = function (applyLast, sign) { 10 | 11 | // check if moment.js is available 12 | var momentJsAvailable = (typeof moment !== 'undefined'); 13 | 14 | //Set class based on sign parameter 15 | signClass = !sign ? "arrow" : sign; 16 | 17 | // set attributes needed for sorting 18 | $('table.sortable').each(function () { 19 | var $this = $(this), 20 | context = lookupSortContext($this), 21 | bsSort = context.bsSort; 22 | applyLast = (applyLast === true); 23 | $this.find('span.sign').remove(); 24 | $this.find('thead tr').each(function (rowIndex) { 25 | var columnsSkipped = 0; 26 | $(this).find('th').each(function (columnIndex) { 27 | var $this = $(this); 28 | $this.attr('data-sortcolumn', columnIndex + columnsSkipped); 29 | $this.attr('data-sortkey', columnIndex + '-' + rowIndex); 30 | if ($this.attr("colspan") !== undefined) { 31 | columnsSkipped += parseInt($this.attr("colspan")) - 1; 32 | } 33 | }); 34 | }); 35 | $this.find('td').each(function () { 36 | var $this = $(this); 37 | if ($this.attr('data-dateformat') !== undefined && momentJsAvailable) { 38 | $this.attr('data-value', moment($this.text(), $this.attr('data-dateformat')).format('YYYY/MM/DD/HH/mm/ss')); 39 | } 40 | else { 41 | $this.attr('data-value') === undefined && $this.attr('data-value', $this.text()); 42 | } 43 | }); 44 | $this.find('thead th[data-defaultsort!="disabled"]').each(function (index) { 45 | var $this = $(this); 46 | var $sortTable = $this.closest('table.sortable'); 47 | $this.data('sortTable', $sortTable); 48 | var sortKey = $this.attr('data-sortkey'); 49 | var thisLastSort = applyLast ? context.lastSort : -1; 50 | bsSort[sortKey] = applyLast ? bsSort[sortKey] : $this.attr('data-defaultsort'); 51 | if (bsSort[sortKey] !== undefined && (applyLast === (sortKey === thisLastSort))) { 52 | bsSort[sortKey] = bsSort[sortKey] === 'asc' ? 'desc' : 'asc'; 53 | doSort($this, $sortTable); 54 | } 55 | }); 56 | $this.trigger('sorted'); 57 | }); 58 | }; 59 | 60 | // add click event to table header 61 | $document.on('click', 'table.sortable thead th[data-defaultsort!="disabled"]', function (e) { 62 | var $this = $(this), $table = $this.data('sortTable') || $this.closest('table.sortable'); 63 | doSort($this, $table); 64 | $table.trigger('sorted'); 65 | }); 66 | 67 | // Look up sorting data appropriate for the specified table (jQuery element). 68 | // This allows multiple tables on one page without collisions. 69 | function lookupSortContext($table) { 70 | var context = $table.data("bootstrap-sortable-context"); 71 | if(context == null) { 72 | context = { bsSort: [], lastSort: null }; 73 | $table.data("bootstrap-sortable-context", context); 74 | } 75 | return context; 76 | } 77 | 78 | //Sorting mechanism separated 79 | function doSort($this, $table) { 80 | var sortColumn = $this.attr('data-sortcolumn'), 81 | context = lookupSortContext($table), 82 | bsSort = context.bsSort; 83 | 84 | var colspan = $this.attr('colspan'); 85 | if (colspan) { 86 | var selector; 87 | for (var i = parseFloat(sortColumn) ; i < parseFloat(sortColumn) + parseFloat(colspan) ; i++) { 88 | selector = selector + ', [data-sortcolumn="' + i + '"]'; 89 | } 90 | var subHeader = $(selector).not('[colspan]'); 91 | var mainSort = subHeader.filter('[data-mainsort]').eq(0); 92 | 93 | sortColumn = mainSort.length ? mainSort : subHeader.eq(0); 94 | doSort(sortColumn, $table); 95 | return; 96 | } 97 | 98 | var localSignClass = $this.attr('data-defaultsign') || signClass; 99 | 100 | // update arrow icon 101 | if ($.browser.mozilla) { 102 | var moz_arrow = $table.find('div.mozilla'); 103 | if (moz_arrow !== undefined) { 104 | moz_arrow.find('.sign').remove(); 105 | moz_arrow.parent().html(moz_arrow.html()); 106 | } 107 | $this.wrapInner('
'); 108 | $this.children().eq(0).append(''); 109 | } 110 | else { 111 | $table.find('span.sign').remove(); 112 | $this.append(''); 113 | } 114 | 115 | // sort direction 116 | var sortKey = $this.attr('data-sortkey'); 117 | var initialDirection = $this.attr('data-firstsort') !== 'desc' ? 'desc' : 'asc'; 118 | 119 | context.lastSort = sortKey; 120 | bsSort[sortKey] = (bsSort[sortKey] || initialDirection) === 'asc' ? 'desc' : 'asc'; 121 | if (bsSort[sortKey] === 'desc') { $this.find('span.sign').addClass('up'); } 122 | 123 | // sort rows 124 | var rows = $table.children('tbody').children('tr'); 125 | rows.tsort('td:eq(' + sortColumn + ')', { order: bsSort[sortKey], attr: 'data-value' }); 126 | 127 | // add class to sorted column cells 128 | $table.find('td.sorted, th.sorted').removeClass('sorted'); 129 | rows.find('td:eq(' + sortColumn + ')').addClass('sorted'); 130 | $this.addClass('sorted'); 131 | } 132 | 133 | // jQuery 1.9 removed this object 134 | if (!$.browser) { 135 | $.browser = { chrome: false, mozilla: false, opera: false, msie: false, safari: false }; 136 | var ua = navigator.userAgent; 137 | $.each($.browser, function (c) { 138 | $.browser[c] = ((new RegExp(c, 'i').test(ua))) ? true : false; 139 | if ($.browser.mozilla && c === 'mozilla') { $.browser.mozilla = ((new RegExp('firefox', 'i').test(ua))) ? true : false; } 140 | if ($.browser.chrome && c === 'safari') { $.browser.safari = false; } 141 | }); 142 | } 143 | 144 | // Initialise on DOM ready 145 | $($.bootstrapSortable); 146 | 147 | }(jQuery)); 148 | -------------------------------------------------------------------------------- /public/js/logs.js: -------------------------------------------------------------------------------- 1 | var socket = io(); 2 | 3 | socket.on('logs', function(data) { 4 | $('#logs').html(data); 5 | }); -------------------------------------------------------------------------------- /public/js/script.js: -------------------------------------------------------------------------------- 1 | // webSockets client side declaration 2 | var websocket = location.pathname + "socket.io"; 3 | var socket = io.connect({ path: websocket }); 4 | 5 | //settings sent to server 6 | // profile : device profile, selected by user. 7 | // url : url of the website which need to be check. 8 | var settings = { 9 | profile: null, 10 | url: null 11 | }; 12 | 13 | var errors = 0; 14 | var warnings = 0; 15 | var infos = 0; 16 | 17 | console.log(window.innerWidth); 18 | 19 | var checkButton = document.getElementById('submit'); 20 | checkButton.addEventListener('click', clickHandler, true); 21 | 22 | window.addEventListener('popstate', function(event) { 23 | console.log('popstate fired!'); 24 | loadHomePage(); 25 | updateContent(event.state); 26 | }); 27 | 28 | function clickHandler(event) { 29 | var data = $('#url').val(); 30 | history.pushState(data, event.target.textContent, event.target.href); 31 | } 32 | 33 | function updateContent(data) { 34 | if(data == null) 35 | return; 36 | checkURI(window.location.search); 37 | 38 | } 39 | //get querystring and return asked url if exist 40 | //this function provide an unique URI to share a report configuration. 41 | // TODO : insert device selector in URI 42 | function checkURI(querystring) { 43 | var query = {}; 44 | var buffer = querystring.slice(1).split('&'); 45 | console.log(querystring); 46 | for (var index in buffer) { 47 | query[buffer[index].split('=')[0]] = buffer[index].split('=')[1]; 48 | } 49 | if (query["url"]) { 50 | document.getElementById('url').value = decodeURIComponent(query["url"]); 51 | if (query["profile"]) { 52 | $("input[value=" + decodeURIComponent(query["profile"]) + "]").click(); 53 | settings.profile = decodeURIComponent(query["profile"]); 54 | } else { 55 | return; 56 | } 57 | } else { 58 | document.getElementById('url').value = ""; 59 | return; 60 | } 61 | } 62 | 63 | //display all home page's elements and hide report page. 64 | //call checkURI function to insert an URI in input element if a query url exist. 65 | function loadHomePage() { 66 | $('#report').removeClass('report'); 67 | $('#report').hide(); 68 | $('#home').removeClass('report'); 69 | $('#sm').show(); 70 | $('#sm').removeClass('screenshot'); 71 | $('#sm2').show(); 72 | $('#sm2').removeClass('screenshot'); 73 | $('#tab').show(); 74 | $('#tab').removeClass('screenshot'); 75 | $('#smartphone').removeClass("sm"); 76 | $('#smartphone').removeClass("sm2"); 77 | $('#smartphone').removeClass("tab"); 78 | $('#console-title').hide(); 79 | $('#console').hide(); 80 | $('#cog1').addClass("active"); 81 | $('#cog2').addClass("active"); 82 | checkURI(window.location.search); 83 | } 84 | 85 | function loadFailurePage() { 86 | $('#cog1').removeClass("active"); 87 | $('#cog2').removeClass("active"); 88 | $('#system-info').html($("

Sorry, it looks like we’ve crashed :(

")); 89 | } 90 | 91 | //display all progress elements, report page and animate smartphone 92 | function loadProgressPage() { 93 | errors = 0; 94 | warnings = 0; 95 | infos = 0; 96 | $('#target').attr('href', document.getElementById('url').value); 97 | $('#target').text(document.getElementById('url').value); 98 | $('#report').show(); 99 | $('#tip-issue-feed').empty(); 100 | $('#info-issue-feed').empty(); 101 | $('#error-issue-feed').empty(); 102 | $('#warning-issue-feed').empty(); 103 | $('#info-issue-feed').hide(); 104 | $('#error-issue-feed').hide(); 105 | $('#warning-issue-feed').hide(); 106 | $('#smartphone').empty(); 107 | $('#cog1').addClass("active"); 108 | $('#cog2').addClass("active"); 109 | $('#system-info').html(''); 110 | $("#inprogress").show("1s"); 111 | var scales = { 112 | 'sm': 8.31, 113 | 'sm2': 8.31, 114 | 'tab': 2.5 115 | }; 116 | var selectedDevice = $('input[name=device]:checked').parent().find('img').eq( 117 | 0); 118 | var id = selectedDevice.attr('id'); 119 | var scale = scales[id];; 120 | var offset2 = selectedDevice.offset(); 121 | var offset1 = $('#smartphone').offset(); 122 | var transformprefixwebkit = '-webkit-transform: translate3d(' + 123 | (offset1.left - offset2.left) + 'px, ' + (offset1.top - offset2.top) + 124 | 'px,0) scale(' + scale + ',' + scale + ') rotate(360deg)'; 125 | var transform = 'transform: translate3d(' + 126 | (offset1.left - offset2.left) + 'px, ' + (offset1.top - offset2.top) + 127 | 'px,0) scale(' + scale + ',' + scale + ') rotate(360deg)'; 128 | var style = $('').appendTo('head'); 130 | var styleprefixed = $('').appendTo('head'); 132 | if($( window ).width() > 990) { 133 | selectedDevice.addClass('screenshot'); 134 | } 135 | $('#home').addClass('report'); 136 | $('#report').addClass('report'); 137 | 138 | if($( window ).width() > 990) { 139 | setTimeout(function() { 140 | $('#smartphone').addClass(id); 141 | selectedDevice.hide(); 142 | selectedDevice.removeClass('screenshot'); 143 | style.remove(); 144 | }, 1000); 145 | } 146 | 147 | } 148 | 149 | //hide progress animation 150 | function loadResultPage() { 151 | $('#cog1').removeClass("active"); 152 | $('#cog2').removeClass("active"); 153 | $("#inprogress").hide("1s"); 154 | $("#tipbody").addClass("collapse"); 155 | $('#navbar-report').addClass("navbar-active"); 156 | } 157 | 158 | //PROTOCOL of client Side 159 | loadHomePage(); 160 | 161 | //detect device choice and add a profile device in settings 162 | $('.device-selector').click(function() { 163 | var id = this.id; 164 | settings.profile = id; 165 | $('#device-selected').text(id); 166 | }); 167 | 168 | //detect form submit, update URI of mobile checker, add URI asked to settings and send data to server side 169 | $('form').submit(function() { 170 | settings.url = $('#url').val(); 171 | settings.profile = $('input[name="device"]:radio:checked').val(); 172 | var url = window.location.origin + window.location.pathname; 173 | url += "?url=" + encodeURIComponent(settings.url); 174 | url += "&profile=" + encodeURIComponent(settings.profile); 175 | window.history.pushState({}, "mobile checker - " + settings.url, url); 176 | socket.emit('check', settings); 177 | return false; 178 | }); 179 | 180 | //server event : inform the check begin 181 | socket.on('start', function(data) { 182 | loadProgressPage(); 183 | }); 184 | 185 | socket.on('tip', function(data) { 186 | //server event : add report header and some infos 187 | var tip = $("
"); 188 | tip.html(data); 189 | $('#tip-issue-feed').append(tip); 190 | }); 191 | 192 | //server event : display console if some problems detected on server side. 193 | //TODO : display all server side errors. For the moment only display errors detected and interpreted via throw function. 194 | socket.on('exception', function(msg) { 195 | $('#console-title').show(); 196 | $('#console').show(); 197 | $('#console').text(msg); 198 | }); 199 | socket.on('unsafeUrl', function(data) { 200 | $('#dns-error').remove(); 201 | $('#errors').append($( 202 | '' 208 | )); 209 | }); 210 | 211 | //server event : detect when check is done. 212 | socket.on('done', function(data) {}); 213 | socket.on('ok', function(data) {}); 214 | 215 | socket.on('disconnect', function() { 216 | loadFailurePage(); 217 | }); 218 | 219 | socket.on('wait', function(data) { 220 | var waitingTime = data * 10; 221 | var waitMessage = "

Hi, this service is receiving too many requests. Waiting time around " +waitingTime+ "s. Please don't leave the page.

"; 222 | $('#system-info').append($(waitMessage)); 223 | }); 224 | 225 | socket.on('jobStarted', function(){ 226 | $('#system-info').empty(); 227 | }); 228 | 229 | socket.on('err', function(data) { 230 | if (data.status == "error") { 231 | if (errors == 0) { 232 | $('#error-issue-feed').show(); 233 | var errortitle = ""; 235 | $('#error-issue-feed').append($(errortitle)); 236 | } 237 | $('#error-issue-feed').append($(data.issue)); 238 | errors++; 239 | } 240 | if (data.status == "warning") { 241 | if (warnings == 0) { 242 | $('#warning-issue-feed').show(); 243 | var warningtitle = ""; 245 | $('#warning-issue-feed').append($(warningtitle)); 246 | } 247 | $('#warning-issue-feed').append($(data.issue)); 248 | warnings++; 249 | } 250 | if (data.status == "info") { 251 | if (infos == 0) { 252 | $('#info-issue-feed').show(); 253 | var infotitle = ""; 255 | $('#info-issue-feed').append($(infotitle)); 256 | } 257 | $('#info-issue-feed').append($(data.issue)); 258 | infos++; 259 | } 260 | $.bootstrapSortable(); 261 | }); 262 | 263 | //server event : get screenshot path when it is ready and display it in smartphone frame. 264 | socket.on('screenshot', function(path) { 265 | $('').attr('src', path).attr('alt', 'Screenshot').attr('id', 266 | 'screenshot').attr('width', 266).appendTo('#smartphone'); 267 | }); 268 | 269 | //server event : detect end of all checks and call loadResultPage function. 270 | socket.on('end', function() { 271 | loadResultPage(); 272 | }); 273 | -------------------------------------------------------------------------------- /public/logs.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | logs 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 19 |
20 |
21 |

Current State

22 |

clients connected: <%= currentState.clients %>

23 |

validations running: <%= currentState.validations %>

24 |
25 |
26 |

History - <%= history.startDate %>

27 |

clients connections: <%= history.clients %>

28 |

validations runs: <%= history.validations %>

29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "W3C Mobile Checker", 3 | "lang": "en", 4 | "start_url": "./", 5 | "icons": [ 6 | {"src": "img/mobile-checker-icon.png", 7 | "sizes": "any", 8 | "type": "image/png" 9 | } 10 | ], 11 | "display": "standalone" 12 | } 13 | -------------------------------------------------------------------------------- /public/octicons/LICENSE.txt: -------------------------------------------------------------------------------- 1 | (c) 2012-2015 GitHub 2 | 3 | When using the GitHub logos, be sure to follow the GitHub logo guidelines (https://github.com/logos) 4 | 5 | Font License: SIL OFL 1.1 (http://scripts.sil.org/OFL) 6 | Applies to all font files 7 | 8 | Code License: MIT (http://choosealicense.com/licenses/mit/) 9 | Applies to all other files 10 | -------------------------------------------------------------------------------- /public/octicons/README.md: -------------------------------------------------------------------------------- 1 | If you intend to install Octicons locally, install `octicons-local.ttf`. It should appear as “github-octicons” in your font list. It is specially designed not to conflict with GitHub's web fonts. 2 | -------------------------------------------------------------------------------- /public/octicons/octicons-local.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/octicons/octicons-local.ttf -------------------------------------------------------------------------------- /public/octicons/octicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/octicons/octicons.eot -------------------------------------------------------------------------------- /public/octicons/octicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/octicons/octicons.ttf -------------------------------------------------------------------------------- /public/octicons/octicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/public/octicons/octicons.woff -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 20000 2 | -------------------------------------------------------------------------------- /test/test_server/public/css/style.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style-type: none; 3 | } 4 | a { 5 | padding-left: 20px; 6 | color: #E54E27; 7 | } 8 | span { 9 | color: #E54E27; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /test/test_server/public/docs/404-manifest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manifest is 404 5 | 6 | 7 | Manifest is 404 8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/404-manifest.json: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manifest is 404 5 | 6 | 7 | Manifest is 404 8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/badjson-manifest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manifest is broken JSON 5 | 6 | 7 | Manifest is bad JSON 8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/badmanifest.json: -------------------------------------------------------------------------------- 1 | { 2 | -------------------------------------------------------------------------------- /test/test_server/public/docs/brokenlinks-manifest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manifest with broken links 5 | 6 | 7 | Manifest with broken links 8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/brokenlinks-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "/url/does/not/exist" 3 | } 4 | -------------------------------------------------------------------------------- /test/test_server/public/docs/brokenlinks-manifest2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manifest with broken links 5 | 6 | 7 | Manifest with broken links 8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/brokenlinks-manifest2.json: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [{"src": "//domain-does-not-exist"}] 3 | } 4 | -------------------------------------------------------------------------------- /test/test_server/public/docs/compressed.html: -------------------------------------------------------------------------------- 1 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

2 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

3 | 4 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

5 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

6 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

7 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/css-inconsistent-prefixes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page with inconsistent CSS prefixes 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/css-prefixes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page with CSS prefixes 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/firework.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/test/test_server/public/docs/firework.jpg -------------------------------------------------------------------------------- /test/test_server/public/docs/font-size-ok.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

16px font size

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/http-errors-favicon.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/test_server/public/docs/http-errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/test_server/public/docs/icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/test/test_server/public/docs/icon1.png -------------------------------------------------------------------------------- /test/test_server/public/docs/icon2.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/test_server/public/docs/inconsistent-prefixes.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-border-radius: 5%; 3 | border-radius: 15%; 4 | } -------------------------------------------------------------------------------- /test/test_server/public/docs/invalid-manifest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Invalid Manifest 5 | 6 | 7 | Invalid Manifest 8 | -------------------------------------------------------------------------------- /test/test_server/public/docs/invalidmanifest.json: -------------------------------------------------------------------------------- 1 | { "name": 1 } 2 | -------------------------------------------------------------------------------- /test/test_server/public/docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mobile checker test manifest", 3 | "start_url": "http://0.0.0.0:3001/docs/width_success.html", 4 | "icons": [{ 5 | "src": "//0.0.0.0:3001/docs/icon1.png" 6 | }, 7 | { 8 | "src": "/docs/icon2.svg" 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/multiple-manifests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Multiple manifests 5 | 6 | 7 | 8 | 9 | Multiple manifests 10 | -------------------------------------------------------------------------------- /test/test_server/public/docs/no-manifest.html: -------------------------------------------------------------------------------- 1 | 2 | Look ma, no manifest! 3 | -------------------------------------------------------------------------------- /test/test_server/public/docs/number-requests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/test_server/public/docs/prefixes.css: -------------------------------------------------------------------------------- 1 | body { 2 | -webkit-columns: 2; 3 | } -------------------------------------------------------------------------------- /test/test_server/public/docs/redirects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/test_server/public/docs/too-small-font-size.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

8px font size

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/too-small-touchscreen-target.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/touchscreen-target-ok.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /test/test_server/public/docs/uncompressed.html: -------------------------------------------------------------------------------- 1 | 2 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

3 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

4 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum

5 | -------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_incorrect-initial-scale.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_incorrect-width.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_many-viewport.html: -------------------------------------------------------------------------------- 1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |

12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 |
20 |

21 | 22 |

23 | 24 | 25 | 26 |

27 | 28 |

29 | 30 | 31 |

32 |

33 |
34 |
35 | 36 |

37 | 38 |
39 |

-------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_no-initial-scale.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_no-meta-viewport.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_no-width.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_server/public/docs/viewport_ok.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_server/public/docs/width_fail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | width_fail.html 6 | 7 | 8 |
width = 300 %
9 | 10 | -------------------------------------------------------------------------------- /test/test_server/public/docs/width_success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | width_success.html 6 | 7 | 8 |
width = 100 %
9 | 10 | -------------------------------------------------------------------------------- /test/test_server/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/test/test_server/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /test/test_server/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/test/test_server/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /test/test_server/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/test/test_server/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /test/test_server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test_server 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Mocha tests sources

15 |
16 | 22 |
    23 | 24 |
  • docname
  • 25 |
  • 26 |
  • 27 |
  • 28 |
  • 29 |
  • 30 |
  • 31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/test_server/public/js/script.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3c/Mobile-Checker/a5f9691f6ff7b29be5d86bea9d11be50954615a4/test/test_server/public/js/script.js -------------------------------------------------------------------------------- /test/test_server/test_app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | app = express(), 3 | http = require('http').Server(app), 4 | compress = require('compression'), 5 | fs = require('fs'); 6 | 7 | 8 | var serverport; 9 | 10 | app.use(compress({ 11 | threshold: 2048 12 | })); 13 | 14 | 15 | app.get('/', function(req, res) { 16 | res.sendfile('index.html'); 17 | }); 18 | app.get('/redirect.css', function(req, res) { 19 | res.redirect('/css/style.css'); 20 | }); 21 | 22 | app.get('/scheme-relative-redirect', function(req, res) { 23 | res.statusCode = 302; 24 | res.setHeader("Location", "//0.0.0.0:" + serverport + "/js/script.js"); 25 | res.end(); 26 | }); 27 | 28 | exports.start = function(port, path) { 29 | path = path || '/public'; 30 | path = __dirname + path; 31 | console.log(path); 32 | app.use(express.static(path)); 33 | 34 | serverport = port || 3001; 35 | http.listen(port, function() { 36 | console.log('listening on *:' + serverport); 37 | }); 38 | }; 39 | 40 | exports.close = function() { 41 | http.close(); 42 | }; 43 | -------------------------------------------------------------------------------- /tools/getListOfCssPrefixEquivalents.js: -------------------------------------------------------------------------------- 1 | var jsdom = require("jsdom"); 2 | var fs = require('fs'); 3 | 4 | // loading equivalence table from 5 | // http://peter.sh/experiments/vendor-prefixed-css-property-overview/ 6 | 7 | jsdom.env( 8 | "http://peter.sh/experiments/vendor-prefixed-css-property-overview/", 9 | function (errors, window) { 10 | var propertiesEquivalences = {}; 11 | var rows = window.document.querySelectorAll("tbody tr"); 12 | for (var i = 0; i < rows.length; i++) { 13 | var row = rows[i]; 14 | var cells = row.querySelectorAll('td'); 15 | // the last cell doesn't interest us at this point 16 | for (var j = 0; j < cells.length - 1; j++) { 17 | var cell = cells[j]; 18 | var prop = cell.textContent.replace(' ⓘ', ''); 19 | if (prop) { 20 | var unprefixed = unprefix(prop); 21 | if (!propertiesEquivalences[unprefixed]) { 22 | propertiesEquivalences[unprefixed] = {values:[], unneeded:false}; 23 | } 24 | if (propertiesEquivalences[unprefixed].values.indexOf(prop) === -1) { 25 | propertiesEquivalences[unprefixed].values.push(prop); 26 | } 27 | if (cell.querySelector("span") && cell.querySelector("span").title === 'Also supports the non-prefixed version. Supported for legacy reasons.') { 28 | propertiesEquivalences[unprefixed].unneeded = true; 29 | } 30 | } 31 | } 32 | } 33 | fs.writeFileSync("lib/css-prefixes.json", JSON.stringify(propertiesEquivalences)); 34 | } 35 | ); 36 | 37 | function unprefix(string) { 38 | return string.replace(/^-[^-]*-/,''); 39 | } 40 | --------------------------------------------------------------------------------