diff --git a/.gitignore b/.gitignore
index 4086ceec52..2b1ffbfeb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,4 +32,5 @@ nbproject
.hg
.svn
.CVS
-.idea
\ No newline at end of file
+.idea
+node_modules
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000000..b8e1f17207
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: node_js
+node_js:
+ - 0.6
\ No newline at end of file
diff --git a/Makefile b/Makefile
index f99b2303f5..9a4ffa4536 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ BOOTSTRAP_RESPONSIVE_LESS = ./less/responsive.less
DATE=$(shell date +%I:%M%p)
CHECK=\033[32m✔\033[39m
+
#
# BUILD DOCS
#
@@ -36,6 +37,17 @@ build:
@echo "Thanks for using Bootstrap,"
@echo "<3 @mdo and @fat\n"
+#
+# RUN JSHINT & QUNIT TESTS IN PHANTOMJS
+#
+
+test:
+ jshint js/*.js --config js/.jshintrc
+ jshint js/tests/unit/*.js --config js/.jshintrc
+ node js/tests/server.js &
+ phantomjs js/tests/phantom.js "http://localhost:3000/js/tests"
+ kill -9 `cat js/tests/pid.txt`
+ rm js/tests/pid.txt
#
# BUILD SIMPLE BOOTSTRAP DIRECTORY
diff --git a/README.md b/README.md
index cab1d7afde..54ea40577f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[Twitter Bootstrap](http://twitter.github.com/bootstrap)
+[Twitter Bootstrap](http://twitter.github.com/bootstrap) [](http://travis-ci.org/twitter/bootstrap)
=================
Bootstrap provides simple and flexible HTML, CSS, and Javascript for popular user interface components and interactions. In other words, it's a front-end toolkit for faster, more beautiful web development. It's created and maintained by [Mark Otto](http://twitter.com/mdo) and [Jacob Thornton](http://twitter.com/fat) at Twitter.
@@ -91,6 +91,9 @@ $ npm install recess uglify-js jshint -g
+ **build** - `make`
Runs the recess compiler to rebuild the `/less` files and compiles the docs pages. Requires recess and uglify-js. Read more in our docs »
++ **test** - `make test`
+Runs jshint and qunit tests headlessly in phantom js (used for ci). Depends on having phatomjs installed.
+
+ **watch** - `make watch`
This is a convenience method for watching just Less files and automatically building them whenever you save. Requires the Watchr gem.
diff --git a/js/tests/index.html b/js/tests/index.html
index 3e6cb9777c..2f8f71b12e 100644
--- a/js/tests/index.html
+++ b/js/tests/index.html
@@ -11,6 +11,9 @@
+
+
+
diff --git a/js/tests/phantom.js b/js/tests/phantom.js
new file mode 100644
index 0000000000..5173ba9a19
--- /dev/null
+++ b/js/tests/phantom.js
@@ -0,0 +1,63 @@
+// Simple phantom.js integration script
+// Adapted from Modernizr
+
+function waitFor(testFx, onReady, timeOutMillis) {
+ var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001 //< Default Max Timout is 3s
+ , start = new Date().getTime()
+ , condition = false
+ , interval = setInterval(function() {
+ if ((new Date().getTime() - start < maxtimeOutMillis) && !condition) {
+ // If not time-out yet and condition not yet fulfilled
+ condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()) //< defensive code
+ } else {
+ if(!condition) {
+ // If condition still not fulfilled (timeout but condition is 'false')
+ console.log("'waitFor()' timeout")
+ phantom.exit(1)
+ } else {
+ // Condition fulfilled (timeout and/or condition is 'true')
+ typeof(onReady) === "string" ? eval(onReady) : onReady() //< Do what it's supposed to do once the condition is fulfilled
+ clearInterval(interval) //< Stop this interval
+ }
+ }
+ }, 100) //< repeat check every 100ms
+}
+
+
+if (phantom.args.length === 0 || phantom.args.length > 2) {
+ console.log('Usage: phantom.js URL')
+ phantom.exit()
+}
+
+var page = new WebPage()
+
+// Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
+page.onConsoleMessage = function(msg) {
+ console.log(msg)
+};
+
+page.open(phantom.args[0], function(status){
+ if (status !== "success") {
+ console.log("Unable to access network")
+ phantom.exit()
+ } else {
+ waitFor(function(){
+ return page.evaluate(function(){
+ var el = document.getElementById('qunit-testresult')
+ if (el && el.innerText.match('completed')) {
+ return true
+ }
+ return false
+ })
+ }, function(){
+ var failedNum = page.evaluate(function(){
+ var el = document.getElementById('qunit-testresult')
+ try {
+ return el.getElementsByClassName('failed')[0].innerHTML
+ } catch (e) { }
+ return 10000
+ });
+ phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0)
+ })
+ }
+})
\ No newline at end of file
diff --git a/js/tests/server.js b/js/tests/server.js
new file mode 100644
index 0000000000..27d1bf24b3
--- /dev/null
+++ b/js/tests/server.js
@@ -0,0 +1,18 @@
+/*
+ * Simple connect server for phantom.js
+ * Adapted from Modernizr
+ */
+
+var connect = require('connect')
+ , args = process.argv.slice(2)
+ , fs = require('fs')
+ , folder = '/../../'
+ , port = '3000'
+
+var server = connect.createServer(
+ connect.static(__dirname + folder)
+).listen(port)
+
+fs.writeFileSync(__dirname + '/pid.txt', process.pid, 'utf-8')
+
+console.log("Server started on port %s in %s", port, folder)
\ No newline at end of file
diff --git a/js/tests/unit/bootstrap-phantom.js b/js/tests/unit/bootstrap-phantom.js
new file mode 100644
index 0000000000..a04aeaa878
--- /dev/null
+++ b/js/tests/unit/bootstrap-phantom.js
@@ -0,0 +1,21 @@
+// Logging setup for phantom integration
+// adapted from Modernizr
+
+QUnit.begin = function () {
+ console.log("Starting test suite")
+ console.log("================================================\n")
+}
+
+QUnit.moduleDone = function (opts) {
+ if (opts.failed === 0) {
+ console.log("\u2714 All tests passed in '" + opts.name + "' module")
+ } else {
+ console.log("\u2716 " + opts.failed + " tests failed in '" + opts.name + "' module")
+ }
+}
+
+QUnit.done = function (opts) {
+ console.log("\n================================================")
+ console.log("Tests completed in " + opts.runtime + " milliseconds")
+ console.log(opts.passed + " tests of " + opts.total + " passed, " + opts.failed + " failed.")
+}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..5f54d5c78a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "bootstrap"
+ , "description": "HTML, CSS, and JS toolkit from Twitter."
+ , "version": "2.0.3"
+ , "keywords": ["bootstrap", "css"]
+ , "homepage": "http://twitter.github.com/bootstrap/"
+ , "author": "Twitter Inc."
+ , "scripts": { "test": "make test" }
+ , "repository": {
+ "type": "git"
+ , "url": "https://github.com/twitter/bootstrap.git"
+ }
+ , "licenses": [
+ {
+ "type": "Apache-2.0"
+ , "url": "http://www.apache.org/licenses/LICENSE-2.0"
+ }
+ ]
+ , "devDependencies": {
+ "uglify-js": "*"
+ , "jshint": "*"
+ , "recess": "*"
+ , "connect": "*"
+ }
+}
\ No newline at end of file