diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..9785b3d --- /dev/null +++ b/.eslintrc @@ -0,0 +1,25 @@ +{ + "rules": { + "indent": [ + 2, + 4 + ], + "quotes": [ + 2, + "single" + ], + "linebreak-style": [ + 2, + "windows" + ], + "semi": [ + 2, + "always" + ] + }, + "env": { + "browser": true, + "jquery": true + }, + "extends": "eslint:recommended" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7dc4f0e..9d1abd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -node_modules \ No newline at end of file +node_modules +private.conf.js \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 3da2d0d..c0f8987 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,23 +1,183 @@ -// TODO: build task -// TODO: minify task -// TODO: eslint task -// TODO: cloud selenium server to test more browsers // TODO: add more test specs (see matchHeight.spec.js) // TODO: travis CI var gulp = require('gulp'); +var uglify = require('gulp-uglify'); +var rename = require('gulp-rename'); +var header = require('gulp-header'); +var eslint = require('gulp-eslint'); +var gulpBump = require('gulp-bump'); +var tag = require('gulp-tag-version'); +var sequence = require('run-sequence'); var webdriver = require('gulp-webdriver'); var webserver = require('gulp-webserver'); +var selenium = require('selenium-standalone'); +var browserStack = require('gulp-browserstack'); +var staticTransform = require('connect-static-transform'); +var privateConfig = require('./test/conf/private.conf.js').config; +var pkg = require('./package.json'); +var server; -gulp.task('test', function() { - return gulp.src('test/conf/wdio.conf.js').pipe(webdriver()); +gulp.task('release', function(callback) { + var type = process.argv[4] || 'minor'; + sequence('lint', 'test', 'build', 'bump:' + type, 'tag', callback); +}); + +gulp.task('build', function() { + return gulp.src(pkg.main) + .pipe(uglify()) + .pipe(header(banner, { pkg: pkg })) + .pipe(rename({ suffix: '-min' })) + .pipe(gulp.dest('.')); +}); + +gulp.task('lint', function() { + return gulp.src(pkg.main) + .pipe(eslint()) + .pipe(eslint.format()) + .pipe(eslint.failAfterError()); +}); + +var bump = function(options) { + return gulp.src(['package.json', 'bower.json']) + .pipe(gulpBump(options)) + .pipe(gulp.dest('.')); +}; + +gulp.task('bump:patch', function() { + return bump({ type: 'patch' }); +}); + +gulp.task('bump:minor', function() { + return bump({ type: 'minor' }); +}); + +gulp.task('bump:major', function() { + return bump({ type: 'major' }); +}); + +gulp.task('tag', function() { + return gulp.src('package.json') + .pipe(tag({ prefix: '' })); }); gulp.task('serve', function() { - gulp.src('.') - .pipe(webserver({ - livereload: true, - directoryListing: true, - open: 'http://localhost:8000/test/page/test.html' - })); -}); \ No newline at end of file + server = gulp.src('.') + .pipe(webserver({ + host: '0.0.0.0', + //livereload: true, + directoryListing: true, + middleware: function(req, res, next) { + var ieMode = (req._parsedUrl.query || '').replace('=',''); + if (ieMode in emulateIEMiddleware) { + emulateIEMiddleware[ieMode](req, res, next); + } else { + next(); + } + } + })); +}); + +gulp.task('selenium', function(done) { + console.log('Setting up Selenium server...'); + selenium.install({ + logger: function(message) { console.log(message); } + }, function(err) { + if (err) { + done(err); + return; + } + console.log('Starting Selenium server...'); + selenium.start(function(err, child) { + console.log('Selenium server started'); + selenium.child = child; + done(err); + }); + }); +}); + +gulp.task('test', ['serve', 'selenium'], function(done) { + var error; + console.log('Starting webdriver...'); + + var finish = function(err) { + console.log('Webdriver stopped'); + selenium.child.kill(); + console.log('Selenium server stopped'); + if (server) { + try { + server.emit('kill'); + } catch(e) {} + console.log('Web server stopped'); + } + done(error || err); + }; + + gulp.src('test/conf/local.conf.js') + .pipe(webdriver()) + .on('error', function(err) { error = err; }) + .on('finish', finish); +}); + +gulp.task('test:cloud', ['serve'], function(done) { + gulp.src('test/conf/cloud.conf.js') + .pipe(browserStack.startTunnel({ + key: privateConfig.key, + hosts: [{ + name: 'localhost', + port: 8000, + sslFlag: 0 + }] + })) + .pipe(webdriver()) + .pipe(browserStack.stopTunnel()) + .on('finish', function(err) { + if (server) { + try { + server.emit('kill'); + } catch(e) {} + console.log('Web server stopped'); + } + done(err); + }); +}); + +gulp.task('test:cloud:all', function(done) { + return gulp + .src('test/conf/cloud-all.conf.js') + .pipe(browserStack.startTunnel({ + key: privateConfig.key, + hosts: [{ + name: 'localhost', + port: 8000, + sslFlag: 0 + }] + })) + .pipe(webdriver()) + .pipe(browserStack.stopTunnel()); +}); + +var banner = [ + '/*', + '* <%= pkg.name %> v<%= pkg.version %> by @liabru', + '* <%= pkg.homepage %>', + '* License <%= pkg.license %>', + '*/', + '' +].join('\n'); + +var emulateIEMiddlewareFactory = function(version) { + return staticTransform({ + root: __dirname, + match: /(.+)\.html/, + transform: function (path, text, send) { + send(text.replace('content="IE=edge,chrome=1"', 'content="IE=' + version + '"')); + } + }); +}; + +var emulateIEMiddleware = { + 'ie8': emulateIEMiddlewareFactory(8), + 'ie9': emulateIEMiddlewareFactory(9), + 'ie10': emulateIEMiddlewareFactory(10) +}; \ No newline at end of file diff --git a/jquery.matchHeight.js b/jquery.matchHeight.js index 99310c3..8109678 100644 --- a/jquery.matchHeight.js +++ b/jquery.matchHeight.js @@ -4,7 +4,7 @@ * License: MIT */ -;(function($) { +;(function($) { // eslint-disable-line no-extra-semi /* * internal */ diff --git a/package.json b/package.json index e3f6005..043f7a8 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,18 @@ }, "devDependencies": { "gulp": "^3.9.0", + "gulp-browserstack": "^1.0.0", + "gulp-eslint": "^1.0.0", + "gulp-header": "^1.7.1", + "gulp-rename": "^1.2.2", + "gulp-tag-version": "^1.3.0", + "gulp-uglify": "^1.4.2", "gulp-webdriver": "^1.0.1", "gulp-webserver": "^0.9.1", "jasmine": "^2.3.2", - "webdriverio": "^3.2.5" + "run-sequence": "^1.1.4", + "selenium-standalone": "^4.7.0", + "webdriverio": "^3.2.5", + "gulp-bump": "^1.0.0" } } diff --git a/test/conf/cloud-all.conf.js b/test/conf/cloud-all.conf.js new file mode 100644 index 0000000..cd58ae8 --- /dev/null +++ b/test/conf/cloud-all.conf.js @@ -0,0 +1,175 @@ +var privateConfig = require('./private.conf.js').config; + +var testUrl = 'http://localhost:8000/test/page/test.html', + viewports = [[1280, 1024], [640, 480], [320, 640]]; + +var capabilities = [ + { + browser: 'ie', + browser_version: '11', + os: 'windows', + os_version: '10' + }, + { + browser: 'ie', + browser_version: '10', + os: 'windows', + os_version: '7' + }, + { + browser: 'ie', + browser_version: '9', + os: 'windows', + os_version: '7' + }, + { + browser: 'ie', + browser_version: '8', + os: 'windows', + os_version: '7', + viewports: [[1280, 1024]] + }, + { + browserName: 'chrome', + os: 'windows', + os_version: '8' + }, + { + browserName: 'firefox', + os: 'windows', + os_version: '8' + }, + { + browser: 'safari', + browser_version: '8' + }, + { + browser: 'safari', + browser_version: '7.1' + }, + { + browser: 'safari', + browser_version: '6.2' + }, + { + browser: 'iPhone', + device: 'iPhone 6', + deviceOrientation: 'portrait' + }, + { + browser: 'iPhone', + device: 'iPhone 6', + deviceOrientation: 'landscape' + }, + { + browser: 'iPhone', + device: 'iPhone 5S', + deviceOrientation: 'portrait' + }, + { + browser: 'iPhone', + device: 'iPhone 5S', + deviceOrientation: 'landscape' + }, + { + browser: 'iPhone', + device: 'iPhone 4S', + deviceOrientation: 'portrait' + }, + { + browser: 'iPhone', + device: 'iPhone 4S', + deviceOrientation: 'landscape' + }, + { + browser: 'iPhone', + device: 'iPhone 5S', + deviceOrientation: 'portrait' + }, + { + browser: 'iPhone', + device: 'iPhone 5S', + deviceOrientation: 'landscape' + }, + { + browser: 'iPad', + device: 'iPad 4th', + deviceOrientation: 'portrait' + }, + { + browser: 'iPad', + device: 'iPad 4th', + deviceOrientation: 'landscape' + }, + { + browser: 'iPad', + device: 'iPad 3rd', + deviceOrientation: 'portrait' + }, + { + browser: 'iPad', + device: 'iPad 3rd', + deviceOrientation: 'landscape' + }, + { + browser: 'iPad', + device: 'iPad 2nd', + deviceOrientation: 'portrait' + }, + { + browser: 'iPad', + device: 'iPad 2nd', + deviceOrientation: 'landscape' + }, + { + browser: 'android', + device: 'Samsung Galaxy S5', + deviceOrientation: 'portrait' + }, + { + browser: 'android', + device: 'Samsung Galaxy S5', + deviceOrientation: 'landscape' + }, + { + browser: 'android', + device: 'Samsung Galaxy Tab 4 10.1', + deviceOrientation: 'portrait' + }, + { + browser: 'android', + device: 'Samsung Galaxy Tab 4 10.1', + deviceOrientation: 'landscape' + } +]; + +for (var i = 0; i < capabilities.length; i += 1) { + var capability = capabilities[i]; + capability['browserstack.local'] = true; + capability['browserstack.debug'] = true; + capability['initialBrowserUrl'] = testUrl; + capability['urls'] = capability['urls'] || [testUrl]; + + if (!capability['deviceOrientation']) { + capability['viewports'] = capability['viewports'] || viewports; + capability['resolution'] = capability['resolution'] || '1680x1050'; + } +} + +exports.config = { + user: privateConfig.user, + key: privateConfig.key, + capabilities: capabilities, + specs: [ + './test/specs/webdriver.spec.js' + ], + logLevel: 'silent', + coloredLogs: true, + waitforTimeout: 60000, + framework: 'jasmine', + reporter: 'spec', + pauseOnFail: false, + jasmineNodeOpts: { + defaultTimeoutInterval: 60000 + } +}; diff --git a/test/conf/cloud.conf.js b/test/conf/cloud.conf.js new file mode 100644 index 0000000..2716af9 --- /dev/null +++ b/test/conf/cloud.conf.js @@ -0,0 +1,69 @@ +var privateConfig = require('./private.conf.js').config; + +var testUrl = 'http://localhost:8000/test/page/test.html', + viewports = [[1280, 1024], [640, 480], [320, 640]]; + +var capabilities = [ + { + browser: 'ie', + browser_version: '8', + os: 'windows', + os_version: '7', + viewports: [[1280, 1024]] + }, + { + browser: 'safari', + browser_version: '8' + }, + { + browser: 'iPhone', + device: 'iPhone 6', + deviceOrientation: 'landscape' + }, + { + browser: 'iPad', + device: 'iPad 4th', + deviceOrientation: 'landscape' + }, + { + browser: 'android', + device: 'Samsung Galaxy S5', + deviceOrientation: 'landscape' + }, + { + browser: 'android', + device: 'Samsung Galaxy Tab 4 10.1', + deviceOrientation: 'landscape' + } +]; + +for (var i = 0; i < capabilities.length; i += 1) { + var capability = capabilities[i]; + capability['browserstack.local'] = true; + capability['browserstack.debug'] = true; + capability['initialBrowserUrl'] = testUrl; + capability['urls'] = capability['urls'] || [testUrl]; + + if (!capability['deviceOrientation']) { + capability['viewports'] = capability['viewports'] || viewports; + capability['resolution'] = capability['resolution'] || '1680x1050'; + } +} + +exports.config = { + user: privateConfig.user, + key: privateConfig.key, + capabilities: capabilities, + specs: [ + './test/specs/webdriver.spec.js' + ], + logLevel: 'silent', + coloredLogs: true, + waitforTimeout: 60000, + framework: 'jasmine', + reporter: 'spec', + pauseOnFail: false, + jasmineNodeOpts: { + defaultTimeoutInterval: 60000 + } +}; diff --git a/test/conf/local.conf.js b/test/conf/local.conf.js new file mode 100644 index 0000000..6ca9580 --- /dev/null +++ b/test/conf/local.conf.js @@ -0,0 +1,43 @@ +var testUrl = 'http://localhost:8000/test/page/test.html', + viewports = [[1280, 1024], [640, 480], [320, 640]]; + +var capabilities = [ + { + browserName: 'chrome' + }, + { + browserName: 'firefox' + }, + { + browserName: 'internet explorer', + urls: [testUrl, testUrl + '?ie=9', testUrl + '?ie=10'], + }, + { + browserName: 'internet explorer', + urls: [testUrl + '?ie=8'], + viewports: [[1280, 1024]] + } +]; + +for (var i = 0; i < capabilities.length; i += 1) { + var capability = capabilities[i]; + capability['initialBrowserUrl'] = testUrl; + capability['urls'] = capability['urls'] || [testUrl]; + capability['viewports'] = capability['viewports'] || viewports; +} + +exports.config = { + capabilities: capabilities, + specs: [ + './test/specs/webdriver.spec.js' + ], + logLevel: 'silent', + coloredLogs: true, + waitforTimeout: 99999999, + framework: 'jasmine', + reporter: 'spec', + pauseOnFail: true, + jasmineNodeOpts: { + defaultTimeoutInterval: 99999999 + } +}; diff --git a/test/conf/wdio.conf.js b/test/conf/wdio.conf.js deleted file mode 100644 index c323e0e..0000000 --- a/test/conf/wdio.conf.js +++ /dev/null @@ -1,24 +0,0 @@ -exports.config = { - capabilities: [ - { - browserName: 'firefox' - }, - { - browserName: 'chrome' - }, - { - browserName: 'internet explorer' - } - ], - specs: [ - './test/specs/webdriver.spec.js' - ], - logLevel: 'silent', - coloredLogs: true, - waitforTimeout: 10000, - framework: 'jasmine', - reporter: 'spec', - jasmineNodeOpts: { - defaultTimeoutInterval: 10000 - } -}; diff --git a/test/specs/matchHeight.spec.js b/test/specs/matchHeight.spec.js index 2fd816f..3bc5f2f 100644 --- a/test/specs/matchHeight.spec.js +++ b/test/specs/matchHeight.spec.js @@ -19,7 +19,7 @@ describe('matchHeight', function() { it('has been defined', function(done) { var matchHeight = $.fn.matchHeight; expect(typeof matchHeight).toBe('function'); - expect(Array.isArray(matchHeight._groups)).toBe(true); + expect(testHelper.isArray(matchHeight._groups)).toBe(true); expect(typeof matchHeight._throttle).toBe('number'); expect(typeof matchHeight._maintainScroll).toBe('boolean'); expect(typeof matchHeight._rows).toBe('function'); @@ -32,14 +32,11 @@ describe('matchHeight', function() { it('has matched heights automatically after images load', function(done) { var $items = $('.image-items'), currentBreakpoint = testHelper.getCurrentBreakpoint(), - image0Width = $items.find('.item-0 img')[0].naturalWidth, item0Height = $items.find('.item-0').outerHeight(), item1Height = $items.find('.item-1').outerHeight(), item2Height = $items.find('.item-2').outerHeight(), item3Height = $items.find('.item-3').outerHeight(); - expect(image0Width).toBe(800); - if (currentBreakpoint === 'mobile') { // all heights will be different } else if (currentBreakpoint === 'tablet') { @@ -61,12 +58,14 @@ describe('matchHeight', function() { expectedNumberCols = 4, expectedNumberRows = 2; - if (currentBreakpoint === 'mobile') { - expectedNumberCols = 1; - expectedNumberRows = 8; - } else if (currentBreakpoint === 'tablet') { - expectedNumberCols = 2; - expectedNumberRows = 4; + if (testHelper.isMediaQueriesSupported) { + if (currentBreakpoint === 'mobile') { + expectedNumberCols = 1; + expectedNumberRows = 8; + } else if (currentBreakpoint === 'tablet') { + expectedNumberCols = 2; + expectedNumberRows = 4; + } } expect(rows.length).toBe(expectedNumberRows); @@ -211,6 +210,8 @@ describe('matchHeight', function() { expect(item2Height).toBe(item3Height); expect(item0Height).not.toBe(item2Height); expect(item1Height).not.toBe(item3Height); + } else { + expect(true).toBe(true); } done(); @@ -234,34 +235,39 @@ describe('matchHeight', function() { jasmine.getEnv().addReporter({ suiteStarted: function() { - window.specsPassed = 0; - window.specsFailed = 0; + window.specsPassed = []; + window.specsFailed = []; $('.test-summary').text('running tests...'); }, specDone: function(result) { if (result.status === 'passed') { - window.specsPassed += 1; + window.specsPassed.push(result.id); } else { - window.specsFailed += 1; + window.specsFailed.push(result.id); } }, suiteDone: function() { $('.test-summary') - .toggleClass('has-passed', window.specsFailed === 0) - .toggleClass('has-failed', window.specsFailed !== 0) - .text(window.specsPassed + ' tests passed, ' + window.specsFailed + ' failed'); + .toggleClass('has-passed', window.specsFailed.length === 0) + .toggleClass('has-failed', window.specsFailed.length !== 0) + .text(window.specsPassed.length + ' tests passed, ' + window.specsFailed.length + ' failed'); } }); var testHelper = { + isMediaQueriesSupported: typeof (window.matchMedia || window.msMatchMedia) !== 'undefined' || navigator.userAgent.indexOf('MSIE 9.0') >= 0, + isArray: function(obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }, getCurrentBreakpoint: function() { - var windowWidth = $(window).width(); - - if (windowWidth <= 640) { - return 'mobile'; - } else if (windowWidth <= 1024) { - return 'tablet'; + if (testHelper.isMediaQueriesSupported) { + var windowWidth = $(window).width(); + if (windowWidth <= 640) { + return 'mobile'; + } else if (windowWidth <= 1024) { + return 'tablet'; + } } return 'desktop'; diff --git a/test/specs/webdriver.spec.js b/test/specs/webdriver.spec.js index 6800034..0867d67 100644 --- a/test/specs/webdriver.spec.js +++ b/test/specs/webdriver.spec.js @@ -1,31 +1,58 @@ +var async = require('async'); + describe('matchHeight webdriver', function() { - var runAllTests = function(done, width, height) { - browser - .setViewportSize({ width: width, height: height }) - .url('http://localhost:8000/test/page/test.html') - .waitForExist('.jasmine_html-reporter', 5000) - .execute(function() { - return { - total: window.specsPassed + window.specsFailed, - passed: window.specsPassed, - failed: window.specsFailed - }; - }) - .then(function(ret) { - expect(ret.value.passed).toBe(ret.value.total, 'number of specs passed'); - expect(ret.value.failed).toBe(0, 'number of specs failed'); - }).call(done); - }; + var runAllTests = function(pageUrls, width, height, done) { + var next = browser; + + if (typeof width === 'number' && typeof height === 'number') { + next = next.setViewportSize({ width: width, height: height }) + .windowHandlePosition({ x: 0, y: 0 }); + } else { + done = width; + } - it('passes matchHeight.spec.js at desktop breakpoint', function(done) { - runAllTests(done, 1280, 1024); - }); + async.eachSeries(pageUrls, function(pageUrl, callback) { + next = next.url(pageUrl) + .waitForExist('.jasmine_html-reporter', 60000) + .execute(function() { + return { + total: window.specsPassed.concat(window.specsFailed), + passed: window.specsPassed, + failed: window.specsFailed + }; + }) + .then(function(ret) { + var message = ret.value.failed.join(', ') + ' failed on ' + pageUrl; + expect(ret.value.passed.length).toBe(ret.value.total.length, message); + + if (browser.options.pauseOnFail && ret.value.passed.length !== ret.value.total.length) { + console.log(message); + console.log('paused on failure...'); + next = next.pause(99999999); + } + }) + .then(callback); + }, done); + }; - it('passes matchHeight.spec.js at tablet breakpoint', function(done) { - runAllTests(done, 640, 480); - }); + var urls = browser.desiredCapabilities.urls, + viewports = browser.desiredCapabilities.viewports; - it('passes matchHeight.spec.js at mobile breakpoint', function(done) { - runAllTests(done, 320, 640); - }); + if (viewports) { + for (var i = 0; i < viewports.length; i += 1) { + (function(width, height) { + it('passes matchHeight.spec.js at viewport ' + width + 'x' + height, function(done) { + runAllTests(urls, width, height, function() { + done(); + }); + }); + })(viewports[i][0], viewports[i][1]); + } + } else { + it('passes matchHeight.spec.js', function(done) { + runAllTests(urls, function() { + done(); + }); + }); + } }); \ No newline at end of file