Initial test suite

This commit is contained in:
Buu Nguyen 2014-11-24 18:56:03 -08:00
parent e1f145e8e2
commit 15911b5177
16 changed files with 606 additions and 60 deletions

26
.jshintrc Normal file
View File

@ -0,0 +1,26 @@
{
"node": true,
"browser": true,
"esnext": true,
"camelcase": true,
"immed": true,
"indent": 2,
"latedef": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"undef": false,
"latedef": "nofunc",
"noempty": true,
"trailing": true,
"maxparams": 20,
"maxdepth": 5,
"smarttabs": true,
"asi": true,
"boss": true,
"laxcomma": true,
"laxbreak": true,
"expr": true,
"eqnull": true,
"loopfunc": true
}

BIN
dist/chrome.crx vendored

Binary file not shown.

BIN
dist/chrome.zip vendored

Binary file not shown.

BIN
dist/firefox.xpi vendored

Binary file not shown.

BIN
dist/opera.nex vendored

Binary file not shown.

View File

@ -1,104 +1,113 @@
var gulp = require('gulp')
, path = require('path')
, merge = require('event-stream').merge
, series = require('stream-series')
, map = require('map-stream')
, Crx = require('crx')
, $ = require('gulp-load-plugins')()
var gulp = require('gulp')
, path = require('path')
, merge = require('event-stream').merge
, map = require('map-stream')
, spawn = require('child_process').spawn
, $ = require('gulp-load-plugins')()
/**
* Public tasks
*/
gulp.task('clean', function() {
gulp.task('clean', function () {
return pipe('./tmp', [$.clean()])
})
gulp.task('build', function(cb) {
gulp.task('build', function (cb) {
$.runSequence('clean', 'css', 'chrome', 'opera', 'safari', 'firefox', cb)
})
gulp.task('default', ['build'], function() {
gulp.task('default', ['build'], function () {
gulp.watch(['./src/**/*'], ['default'])
})
gulp.task('dist', ['build'], function(cb) {
gulp.task('dist', ['build'], function (cb) {
$.runSequence('firefox:xpi', 'chrome:zip', 'chrome:crx', 'opera:nex', cb)
})
gulp.task('test', function (cb) {
var ps = spawn(
'./node_modules/.bin/mocha',
['--harmony', '--reporter', 'spec', '--bail', '--recursive', '--timeout', '-1']
)
ps.stdout.pipe(process.stdout);
ps.stderr.pipe(process.stderr);
ps.on('close', cb)
})
/**
* Private tasks
*/
gulp.task('css', function() {
return pipe('./src/octotree.less', [$.less(), $.autoprefixer({ cascade: true })], './tmp')
gulp.task('css', function () {
return pipe('./src/octotree.less', [$.less(), $.autoprefixer({cascade: true})], './tmp')
})
// Chrome
gulp.task('chrome:template', function() {
return buildTemplate({ CHROME: true })
gulp.task('chrome:template', function () {
return buildTemplate({CHROME: true})
})
gulp.task('chrome:js', ['chrome:template'], function() {
return buildJs(['./src/chrome/storage.js'], { CHROME: true })
gulp.task('chrome:js', ['chrome:template'], function () {
return buildJs(['./src/chrome/storage.js'], {CHROME: true})
})
gulp.task('chrome', ['chrome:js'], function() {
gulp.task('chrome', ['chrome:js'], function () {
return merge(
pipe('./icons/**/*', './tmp/chrome/icons'),
pipe(['./libs/**/*', './tmp/octotree.*', './src/chrome/**/*', '!./src/chrome/storage.js'], './tmp/chrome/')
)
})
gulp.task('chrome:zip', function() {
gulp.task('chrome:zip', function () {
return pipe('./tmp/chrome/**/*', [$.zip('chrome.zip')], './dist')
})
gulp.task('chrome:_crx', function(cb) {
$.run('"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"' +
' --pack-extension=' + path.join(__dirname, './tmp/chrome') +
' --pack-extension-key=' + path.join(process.env.HOME, '.ssh/chrome.pem')
gulp.task('chrome:_crx', function (cb) {
$.run('"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"' +
' --pack-extension=' + path.join(__dirname, './tmp/chrome') +
' --pack-extension-key=' + path.join(process.env.HOME, '.ssh/chrome.pem')
).exec(cb)
})
gulp.task('chrome:crx', ['chrome:_crx'], function() {
gulp.task('chrome:crx', ['chrome:_crx'], function () {
return pipe('./tmp/chrome.crx', './dist')
})
// Opera
gulp.task('opera', ['chrome'], function() {
gulp.task('opera', ['chrome'], function () {
return pipe('./tmp/chrome/**/*', './tmp/opera')
})
gulp.task('opera:nex', function() {
gulp.task('opera:nex', function () {
return pipe('./dist/chrome.crx', [$.rename('opera.nex')], './dist')
})
// Safari
gulp.task('safari:template', function() {
return buildTemplate({ SAFARI: true })
gulp.task('safari:template', function () {
return buildTemplate({SAFARI: true})
})
gulp.task('safari:js', ['safari:template'], function() {
return buildJs(['./src/safari/storage.js'], { SAFARI: true })
gulp.task('safari:js', ['safari:template'], function () {
return buildJs(['./src/safari/storage.js'], {SAFARI: true})
})
gulp.task('safari', ['safari:js'], function() {
gulp.task('safari', ['safari:js'], function () {
return merge(
pipe('./icons/**/*', './tmp/safari/octotree.safariextension/icons'),
pipe(['./libs/**/*', './tmp/octotree.js', './tmp/octotree.css',
'./src/safari/**/*', '!./src/safari/storage.js'], './tmp/safari/octotree.safariextension/')
pipe(['./libs/**/*', './tmp/octotree.js', './tmp/octotree.css',
'./src/safari/**/*', '!./src/safari/storage.js'], './tmp/safari/octotree.safariextension/')
)
})
// Firefox
gulp.task('firefox:template', function() {
return buildTemplate({ FIREFOX: true })
gulp.task('firefox:template', function () {
return buildTemplate({FIREFOX: true})
})
gulp.task('firefox:js', ['firefox:template'], function() {
return buildJs(['./src/firefox/storage.js'], { FIREFOX: true })
gulp.task('firefox:js', ['firefox:template'], function () {
return buildJs(['./src/firefox/storage.js'], {FIREFOX: true})
})
gulp.task('firefox', ['firefox:js'], function() {
gulp.task('firefox', ['firefox:js'], function () {
return merge(
pipe('./icons/**/*', './tmp/firefox/data/icons'),
pipe(['./libs/**/*', './tmp/octotree.js', './tmp/octotree.css'], './tmp/firefox/data'),
@ -107,7 +116,7 @@ gulp.task('firefox', ['firefox:js'], function() {
)
})
gulp.task('firefox:xpi', function(cb) {
gulp.task('firefox:xpi', function (cb) {
$.run('cd ./tmp/firefox && cfx xpi --output-file=../../dist/firefox.xpi').exec(cb)
})
@ -120,7 +129,7 @@ function pipe(src, transforms, dest) {
transforms = null
}
var stream = gulp.src(src)
transforms && transforms.forEach(function(transform) {
transforms && transforms.forEach(function (transform) {
stream = stream.pipe(transform)
})
if (dest) stream = stream.pipe(gulp.dest(dest))
@ -134,8 +143,8 @@ function html2js(template) {
var path = $.util.replaceExtension(file.path, '.js')
, content = file.contents.toString()
, escaped = content.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'")
.replace(/\r?\n/g, "\\n' +\n '")
.replace(/'/g, "\\'")
.replace(/\r?\n/g, "\\n' +\n '")
, body = template.replace('$$', escaped)
file.path = path
file.contents = new Buffer(body)
@ -159,13 +168,13 @@ function buildJs(additions, ctx) {
])
return pipe(src, [
$.concat('octotree.js'),
$.preprocess({ context: ctx })
$.preprocess({context: ctx})
], './tmp')
}
function buildTemplate(ctx) {
return pipe('./src/template.html', [
$.preprocess({ context: ctx }),
$.preprocess({context: ctx}),
html2js('const TEMPLATE = \'$$\'')
], './tmp')
}

View File

@ -32,6 +32,16 @@
"gulp-run": "^1.6.4",
"gulp-zip": "^2.0.1",
"crx": "^0.4.4",
"gulp-rename": "^1.2.0"
"gulp-rename": "^1.2.0",
"mocha": "^2.0.1",
"selenium-webdriver": "^2.44.0",
"gulp-mocha": "^2.0.0",
"gulp-exit": "0.0.2",
"starx": "~0.1.7",
"request": "~2.48.0",
"underscore": "~1.7.0",
"underscore.string": "~2.4.0",
"firefox-profile": "~0.3.6",
"async": "~0.9.0"
}
}

View File

@ -31,7 +31,7 @@ const
WIDTH : 250,
POPUP : false,
SHOWN : false,
NONCODE : false,
NONCODE : false,
}
, EVENT = {

View File

@ -12,11 +12,11 @@ HelpPopup.prototype.show = function() {
$view.css('display', 'block').appendTo($('body'))
$(document).one(EVENT.TOGGLE, hide)
setTimeout(function() {
store.set(STORE.POPUP, true)
$view.addClass('show').click(hide)
$(document).one(EVENT.TOGGLE, hide)
setTimeout(hide, 5000)
setTimeout(hide, 12000)
}, 500)
function hide() {

View File

@ -44,22 +44,25 @@ function OptionsView($dom, store) {
*/
// @ifdef CHROME
var $ta = $view.find('[data-store=GHEURLS]')
, urls = $ta.val().split(/\n/)
chrome.runtime.sendMessage({ type: 'requestPermissions', urls: urls }, function(granted) {
if (granted) saveOptions()
else {
// permissions not granted (by user or error), reset value
$ta.val(store.get(STORE.GHEURLS))
saveOptions()
}
})
, urls = $ta.val().split(/\n/).filter(function (url) { return url !== '' })
if (urls.length > 0) {
chrome.runtime.sendMessage({type: 'requestPermissions', urls: urls}, function (granted) {
if (granted) saveOptions()
else {
// permissions not granted (by user or error), reset value
$ta.val(store.get(STORE.GHEURLS))
saveOptions()
}
})
return
}
// @endif
// @ifndef CHROME
saveOptions()
// @endif
return saveOptions()
function saveOptions() {
console.log('save')
var changes = {}
eachOption(
function($elm, key, local, value, cb) {

32
test/factory.js Normal file
View File

@ -0,0 +1,32 @@
require('./helper')
const SELENIUM_SERVER_PATH = path.resolve(__dirname, './selenium/selenium-server-standalone-2.43.1.jar')
const CHROME_DRIVER_PATH = path.resolve(__dirname, './selenium/chromedriver')
const CHROME_CRX_PATH = path.resolve(__dirname, '../dist/chrome.crx')
const FIREFOX_XPI_PATH = path.resolve(__dirname, '../dist/firefox.xpi')
exports.chromeDriver = function (cb) {
var options = new chrome.Options()
options.addExtensions(CHROME_CRX_PATH)
var service = new chrome.ServiceBuilder(CHROME_DRIVER_PATH).build()
cb(null, chrome.createDriver(options, service))
}
exports.firefoxDriver = function (cb) {
var server = new SeleniumServer(SELENIUM_SERVER_PATH, {port: 4444})
server.start()
var profile = new FirefoxProfile();
profile.addExtension(FIREFOX_XPI_PATH, function () {
profile.encoded(function (profile) {
var capabilities = webdriver.Capabilities.firefox()
capabilities.set('firefox_profile', profile);
var driver = new webdriver.Builder()
.usingServer(server.address())
.withCapabilities(capabilities)
.build()
driver.server = server
cb(null, driver)
})
})
}

52
test/helper.js Normal file
View File

@ -0,0 +1,52 @@
global.assert = require('assert')
global.path = require('path')
global.request = require('request')
global.async = require('async')
global._ = require('underscore')
global._s = require('underscore.string')
global.starx = require('starx')
global.sleep = starx.sleep
global.yieldable = starx.yieldable
global.webdriver = require('selenium-webdriver')
global.test = require('selenium-webdriver/testing')
global.chrome = require('selenium-webdriver/chrome')
global.firefox = require('selenium-webdriver/firefox')
global.FirefoxProfile = require('firefox-profile')
global.SeleniumServer = require('selenium-webdriver/remote').SeleniumServer
global.$tag = webdriver.By.tagName
global.$css = webdriver.By.css
global.$id = webdriver.By.id
global.$xpath = webdriver.By.xpath
global.$class = webdriver.By.className
global.yit = function (title, block) {
it(title, function (cb) {
starx(block)(cb)
})
}
global.rand = function (arr) {
return arr[Math.floor(Math.random() * arr.length)]
}
global.getJson = function (url, token, cb) {
var headers = {
'User-Agent': 'buunguyen/octotree (unit test)'
}
if (token) headers.Authorization = 'token ' + token
request({
url : url,
headers: headers
}, function (err, response, body) {
if (err) return cb(err)
cb(null, JSON.parse(body))
})
}
global.isGenerator = function (fn) {
return fn.constructor.name === 'GeneratorFunction'
}

160
test/pageobject.js Normal file
View File

@ -0,0 +1,160 @@
require('./helper')
module.exports = PageObject
/**
* PageObject
* @param driver
* @param repoUrl
* @constructor
*/
function PageObject(driver, repoUrl) {
this.driver = driver
this.one = driver.findElement.bind(driver)
this.all = driver.findElements.bind(driver)
this.repoUrl = repoUrl
}
PageObject.prototype = {
getUrl: function *() {
return yield this.driver.getCurrentUrl()
},
setUrl: function *(url) {
var driver = this.driver
driver.get(url)
yield driver.wait(function () {
return driver.getCurrentUrl().then(function (_url) {
return url === _url
})
}, 5000)
},
close: function *() {
if (this.driver.server) this.driver.server.close()
yield this.driver.quit()
},
reset: function *() {
yield this.setUrl(this.repoUrl)
},
refresh: function *() {
yield this.driver.navigate().refresh()
yield sleep(100)
},
toggleSidebar: function *() {
yield this.toggleButton.click()
yield sleep(100) // transition
},
toggleOptsView: function *() {
yield this.optsButton.click()
},
isSidebarShown: function *() {
var hasCssClass = yield this.driver.isElementPresent($css('html.octotree'))
var btnRight = yield this.toggleButton.getCssValue('right')
return hasCssClass && btnRight === '5px'
},
isSidebarHidden: function *() {
var hasCssClass = yield this.driver.isElementPresent($css('html.octotree'))
var btnRight = yield this.toggleButton.getCssValue('right')
return !hasCssClass && btnRight === '-35px'
},
saveSettings: function *() {
yield this.saveButton.click()
yield sleep(500) // transition + async storage
},
nodeFor: function (path) {
return this.one($id('octotree' + path))
},
childrenOfNode: function (node) {
return node.findElements($css('.jstree-children li'))
},
isNodeSelected: function *(node) {
return yield node.findElement($css('.jstree-wholerow-clicked')).isDisplayed()
},
isNodeOpen: function *(node) {
var classes = yield node.getAttribute('class')
return ~classes.indexOf('jstree-open')
}
}
// UI elements
var controls = {
ghBreadcrumb : '.breadcrumb .final-path',
ghSearch : '.js-site-search-field',
helpPopup : '.octotree_popup',
toggleButton : '.octotree_toggle',
sidebar : '.octotree_sidebar',
treeView : '.octotree_treeview',
treeHeaderLinks : '.octotree_header_repo a',
treeNodes : '.jstree .jstree-node',
optsButton : '.octotree_opts',
optsView : '.octotree_optsview',
tokenInput : '//input[@data-store="TOKEN"]',
hotkeysInput : '//input[@data-store="HOTKEYS"]',
rememberCheck : '//input[@data-store="REMEMBER"]',
nonCodeCheck : '//input[@data-store="NONCODE"]',
saveButton : '.octotree_optsview .button',
errorView : '.octotree_errorview',
errorViewHeader : '.octotree_errorview .octotree_view_header'
}
Object.keys(controls).forEach(function (name) {
var selector = controls[name]
, met = name.indexOf('s') === name.length - 1 ? 'all' : 'one'
, sel = selector.indexOf('//') === 0 ? $xpath : $css
, cond = sel(selector)
Object.defineProperty(PageObject.prototype, name, {
get: function () {
return new WebElementPromiseWrapper(this.driver, this[met](cond), cond)
}
})
})
/**
* WebElementPromiseWrapper
* @param driver
* @param wep
* @param cond
* @constructor
*/
function WebElementPromiseWrapper(driver, wep, cond) {
this.driver = driver
this.wep = wep
this.cond = cond
}
WebElementPromiseWrapper.prototype.then = function () {
return this.wep.then.apply(this.wep, arguments)
}
Object.keys(webdriver.WebElement.prototype).forEach(function (prop) {
if (_.isFunction(webdriver.WebElement.prototype[prop])) {
WebElementPromiseWrapper.prototype[prop] = function *() {
var driver = this.driver
, wep = this.wep
, cond = this.cond
yield driver.wait(function () {
return driver.isElementPresent(cond) // TODO: vary condition per action
}, 5000)
var elm = yield wep
return yield elm[prop].apply(elm, arguments)
}
}
})

BIN
test/selenium/chromedriver Executable file

Binary file not shown.

Binary file not shown.

254
test/specs.js Normal file
View File

@ -0,0 +1,254 @@
require('./helper')
var factory = require('./factory')
, PageObject = require('./pageobject')
, token = process.env.GHPAT
, files
before(function (cb) {
getJson('https://api.github.com/repos/buunguyen/octotree/git/trees/master?recursive=true', token, function (err, data) {
if (err) return cb(err)
files = data.tree
cb()
})
})
;['chrome'/*, 'firefox'*/].forEach(runTest)
function runTest(browser) {
var driver, po
describe(browser, function () {
before(function (cb) {
starx(function *() {
driver = yield factory[browser + 'Driver']
po = new PageObject(driver, 'https://github.com/buunguyen/octotree')
})(cb)
})
after(function (cb) {
starx(function *() {
yield po.close()
})(cb)
})
describe('init', function () {
before(function (cb) {
starx(function *() {
yield po.reset()
})(cb)
})
yit('should create toggle button and sidebar', function *() {
assert.ok(yield po.toggleButton.isDisplayed())
assert.equal(yield po.sidebar.getCssValue('width'), '251px')
})
yit('should display help popup', function *() {
assert.ok(yield po.helpPopup.isDisplayed())
})
yit('should toggle upon button click', function *() {
yield po.toggleSidebar()
assert.ok(yield po.isSidebarShown())
yield po.toggleSidebar()
assert.ok(yield po.isSidebarHidden())
})
})
describe('main', function () {
before(function (cb) {
starx(function *() {
yield po.reset()
yield po.toggleSidebar()
if (token) {
yield po.toggleOptsView()
yield po.tokenInput.sendKeys(token)
yield po.saveSettings()
}
})(cb)
})
describe('tree', function () {
yit('should show tree view by default', function *() {
assert.ok(~(yield po.treeView.getAttribute('class')).indexOf('current'))
})
yit('should show repository information', function *() {
var links = yield po.treeHeaderLinks
assert.equal(yield links[0].getText(), 'buunguyen')
assert.equal(yield links[1].getText(), 'octotree')
})
yit('should show code tree', function *() {
var nodes = yield po.treeNodes
var _files = files.filter(function (file) {
return file.path.indexOf('/') === -1
})
assert.ok(nodes.length, _files.length)
})
yit('should navigate to code files', function *() {
var seen = []
for (var i = 0; i < 3; i++) {
var someFile = rand(files.filter(function (file) {
return file.type === 'blob' && file.path.indexOf('/') === -1 && seen.indexOf(file) === -1
}))
seen.push(someFile)
var node = po.nodeFor(someFile.path)
yield node.click()
assert.ok(yield po.isNodeSelected(node))
assert.equal(yield po.getUrl(), 'https://github.com/buunguyen/octotree/blob/master/' + someFile.path)
yield sleep(200) // pjax
assert.equal(yield po.ghBreadcrumb.getText(), someFile.path)
}
})
yit('should expand directories', function *() {
var seen = []
for (var i = 0; i < 3; i++) {
var someDir = rand(files.filter(function (file) {
return file.type === 'tree' && file.path.indexOf('/') === -1 && seen.indexOf(file) === -1
}))
seen.push(someDir)
var someDirChildren = files.filter(function (file) {
return file.path.indexOf(someDir.path) === 0 && _s.count(file.path, '/') === 1
})
yield po.nodeFor(someDir.path).click()
yield sleep(50) // tree expand transition
assert.ok(yield po.isNodeOpen(po.nodeFor(someDir.path)))
assert.equal((yield po.childrenOfNode(po.nodeFor(someDir.path))).length, someDirChildren.length)
}
})
xit('should match branch', function () {
})
})
describe('opts', function () {
yit('should toggle options view', function *() {
yield po.toggleOptsView()
assert.ok(~(yield po.optsButton.getAttribute('class')).indexOf('selected'))
assert.ok(~(yield po.optsView.getAttribute('class')).indexOf('current'))
yield po.toggleOptsView()
assert.ok(!~(yield po.optsButton.getAttribute('class')).indexOf('selected'))
assert.ok(!~(yield po.optsView.getAttribute('class')).indexOf('current'))
})
describe('values', function () {
beforeEach(function(cb) {
starx(function *() {
yield po.toggleOptsView()
yield po.tokenInput.clear()
if (token) yield po.tokenInput.sendKeys(token)
yield po.hotkeysInput.clear()
if (yield po.rememberCheck.isSelected()) yield po.rememberCheck.click()
if (yield po.nonCodeCheck.isSelected()) yield po.nonCodeCheck.click()
yield po.saveSettings()
})(cb)
})
describe('token', function () {
yit('should show error if token is invalid', function *() {
yield po.toggleOptsView()
yield po.tokenInput.clear()
yield po.tokenInput.sendKeys('invalid token')
yield po.saveSettings()
assert.ok(~(yield po.errorView.getAttribute('class')).indexOf('current'))
assert.ok(~(yield po.errorViewHeader.getText(), 'Error: Invalid token'))
})
yit('should not show error if no token is given', function *() {
yield po.toggleOptsView()
yield po.tokenInput.clear()
yield po.saveSettings()
assert.ok(~(yield po.treeView.getAttribute('class')).indexOf('current'))
})
})
describe('hotkey', function () {
yit('should allow configure hotkey', function *() {
yield po.toggleOptsView()
yield po.hotkeysInput.clear()
yield po.hotkeysInput.sendKeys('`')
yield po.saveSettings()
// Hack: get error when sending key if not focusing on an element first
yield po.ghSearch.sendKeys('`')
yield sleep(100) // transition
assert.ok(yield po.isSidebarHidden())
yield po.ghSearch.sendKeys('`')
yield sleep(100) // transition
assert.ok(yield po.isSidebarShown())
})
})
describe('remember', function () {
yit('should remember sidebar state after reload', function *() {
yield po.toggleOptsView()
yield po.rememberCheck.click()
yield po.saveSettings()
yield po.refresh()
assert.ok(yield po.isSidebarShown())
})
})
describe('non-code', function () {
var pages = [
'https://github.com/buunguyen/octotree/issues',
'https://github.com/buunguyen/octotree/pulls',
'https://github.com/buunguyen/octotree/pulse',
'https://github.com/buunguyen/octotree/graphs/contributors'
]
yit('should hide in non-code pages', function *() {
for (var i = 0; pages[i]; i++) {
yield po.setUrl(pages[i])
assert.ok(yield po.isSidebarHidden())
}
yield po.reset()
yield po.toggleSidebar()
})
yit('should show in non-code pages if option is set', function *() {
yield po.toggleOptsView()
yield po.rememberCheck.click()
yield po.nonCodeCheck.click()
yield po.saveSettings()
for (var i = 0; pages[i]; i++) {
yield po.setUrl(pages[i])
assert.ok(yield po.isSidebarShown())
}
yield po.reset()
})
})
describe('lazy load', function () {
})
describe('collapse', function () {
})
describe('ghe', function () {
})
})
})
})
})
}