diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a869e12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/user.json +/wdio.*.js +classlist.js-*.tgz +node_modules/ +package-lock.json +package/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..d48f2d7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +.travis.yml +classlist.js-*.tgz +package/ +test/ +user.json +wdio.*.js diff --git a/.travis.yml b/.travis.yml index 3147878..267ae50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -script: script/test +script: npm run test:ci language: node_js node_js: -- '0.10' +- 6 diff --git a/README.md b/README.md index 6dc1ef9..013957d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,56 @@ -classList.js is a cross-browser JavaScript shim that fully implements `element.classList`. Refer to [the MDN page on `element.classList`][1] for more information. +# classList.js -This works in every browser except IE 7 or earlier. +[![Test Status](https://saucelabs.com/buildstatus/classlist-polyfill)][sauce] -classList.js is available on these public CDN URLs, allowing you to use this already small file at nearly zero size overhead. +[sauce]: https://saucelabs.com/u/classlist-polyfill - - - - +Cross-browser JavaScript shim that fully implements `element.classList`. -If you would like other versions (such as the current one) hosted there, follow the instructions at -https://github.com/jsdelivr/jsdelivr -and -https://github.com/cdnjs/cdnjs -to prepare a pull request. +Refer to [the MDN page on `element.classList`][MDN] for more information. -![Tracking image](https://in.getclicky.com/212712ns.gif) +[MDN]: https://developer.mozilla.org/en/DOM/element.classList - [1]: https://developer.mozilla.org/en/DOM/element.classList "MDN / DOM / element.classList" + +## Development + +### Getting started + +``` +npm install +``` + +### Run the tests using a local web driver + +``` +npm test +``` + +### Run the tests using cloud browsers + +Requires a [Sauce Labs][] username and access key. + +``` +echo '{"user": "", "key": "" }' > user.json +npm test:ci +``` + +[Sauce Labs]: https://saucelabs.com/ + +### Run the tests using custom configuration + +The tests use [webdriver.io][]. + +Create a `wdio.conf.js`: + +``` +wdio config +# or extend an existing config +cp test/wdio.sample.js wdio.conf.js +``` + +Run the tests: +``` +wdio +``` + +[webdriver.io]: http://webdriver.io/ diff --git a/package.json b/package.json index c7ab5df..f1eab92 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "description": "Cross-browser JavaScript shim that fully implements element.classList (referenced on MDN)", "main": "classList.js", "directories": { - "test": "tests" + "test": "wdio test/wdio.local.js" }, "scripts": { - "test": "bash ./script/test" + "test": "wdio test/wdio.standalone.js", + "test:ci": "wdio test/wdio.sauce.js" }, "repository": { "type": "git", @@ -20,9 +21,32 @@ "cross-browser" ], "author": "me@eligrey.com", + "contributors": [ + "Eli Grey ", + "Doug Beck " + ], "license": "Unlicense", "bugs": { "url": "https://github.com/eligrey/classList.js/issues" }, - "homepage": "https://github.com/eligrey/classList.js#readme" + "homepage": "https://github.com/eligrey/classList.js#readme", + "jshintConfig": { + "esnext": true, + "node": true, + "mocha": true, + "globals": { + "agent": true, + "browser": true, + "expect": true + } + }, + "devDependencies": { + "chai": "^4.1.0", + "wdio-mocha-framework": "^0.5.10", + "wdio-sauce-service": "^0.4.0", + "wdio-selenium-standalone-service": "0.0.9", + "wdio-spec-reporter": "^0.1.0", + "wdio-static-server-service": "^1.0.1", + "webdriverio": "^4.8.0" + } } diff --git a/script/test b/script/test deleted file mode 100755 index e3508e8..0000000 --- a/script/test +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e - -if [ -t 1 ]; then - red="$(printf "\033[31m")" - brightred="$(printf "\033[31;1m")" - green="$(printf "\033[32m")" - reset="$(printf "\033[m")" -else - red= - brightred= - green= - reset= -fi - -phantomjs tests/runner.coffee tests/qunit.html | sed -E " - # failure line: - s/^(✘.+)/${red}\\1${reset}/ - # failure details: - s/^( .+)/${brightred}\\1${reset}/ - # success marker: - s/(✔︎)/${green}\\1${reset}/ -" diff --git a/test/specs/add.html b/test/specs/add.html new file mode 100644 index 0000000..a45e179 --- /dev/null +++ b/test/specs/add.html @@ -0,0 +1,29 @@ + + + + + classList.add + + + +

classList.add

+ +
+
+ + +
+
+ + + + + diff --git a/test/specs/add.js b/test/specs/add.js new file mode 100644 index 0000000..220d081 --- /dev/null +++ b/test/specs/add.js @@ -0,0 +1,27 @@ +'use strict'; + +describe('.classList.add', function() { + + before(function() { + browser.url('test/specs/add.html'); + const ab = browser.getAttribute('#native-ab', 'class'); + if( + browser.getAttribute('#native-simple', 'class') === 'supported' && + ab.includes('a') && ab.includes('b') + ) { + browser.logger.info(`${agent} skipping .add`); + this.skip(); + } + }); + + it('adds class "all-good"', function() { + const classStr = browser.getAttribute('#polyfilled-simple', 'class'); + expect(classStr).to.equal('all-good'); + }); + + it('adds classes "a" and "b"', function() { + const ab = browser.getAttribute('#polyfilled-ab', 'class'); + expect(ab).to.include('a'); + expect(ab).to.include('b'); + }); +}); diff --git a/test/specs/remove.html b/test/specs/remove.html new file mode 100644 index 0000000..e7590d0 --- /dev/null +++ b/test/specs/remove.html @@ -0,0 +1,33 @@ + + + + + classList.remove + + + +

classList.remove

+ +
+
+
+ + +
+
+
+ + + + + diff --git a/test/specs/remove.js b/test/specs/remove.js new file mode 100644 index 0000000..db3bade --- /dev/null +++ b/test/specs/remove.js @@ -0,0 +1,30 @@ +'use strict'; + +describe('.classList.remove', function() { + + before(function() { + browser.url('test/specs/remove.html'); + if( + browser.getAttribute('#native-simple', 'class') === 'hi' && + browser.getAttribute('#native-repeat', 'class') === '' && + browser.getAttribute('#native-list', 'class').includes('yo') == false + ) { + browser.logger.info(`${agent} skipping .remove`); + this.skip(); + } + }); + + it('removes "yo"', function() { + expect(browser.getAttribute('#polyfilled-simple', 'class')).to.equal('hi'); + }); + + it('removes all instances of "yo"', function() { + expect(browser.getAttribute('#polyfilled-repeat', 'class')).to.equal(''); + }); + + it('removes "yo" from a list of classes', function() { + const classStr = browser.getAttribute('#polyfilled-list', 'class'); + expect(classStr).to.not.include('yo'); + }); + +}); diff --git a/test/specs/svg.html b/test/specs/svg.html new file mode 100644 index 0000000..e889b63 --- /dev/null +++ b/test/specs/svg.html @@ -0,0 +1,41 @@ + + + + + svgElement.classList + + + + +

svgElement.classList

+ + + + + + + + + + + + + + + + diff --git a/test/specs/svg.js b/test/specs/svg.js new file mode 100644 index 0000000..3cffc8c --- /dev/null +++ b/test/specs/svg.js @@ -0,0 +1,18 @@ +'use strict'; + +describe('svgElement.classList', function() { + + before(function() { + browser.url('test/specs/svg.html'); + if(browser.getAttribute('#native', 'class') == 'green') { + browser.logger.info(`${agent} skipping svg .classList`); + this.skip(); + } + }); + + it('has "red" removed and "green" toggled on', function() { + const classStr = browser.getAttribute('#polyfilled', 'class'); + expect(classStr).to.equal('green'); + }); + +}); diff --git a/test/specs/toggle.html b/test/specs/toggle.html new file mode 100644 index 0000000..c0e7117 --- /dev/null +++ b/test/specs/toggle.html @@ -0,0 +1,59 @@ + + + + + classList.toggle + + + +

classList.remove

+ + +
+
+
+
+ + + +
+
+
+
+ + + + + + diff --git a/test/specs/toggle.js b/test/specs/toggle.js new file mode 100644 index 0000000..112a0c2 --- /dev/null +++ b/test/specs/toggle.js @@ -0,0 +1,81 @@ +'use strict'; + +describe('.classList.toggle', function() { + + before(function() { + browser.url('test/specs/toggle.html'); + if( + browser.getAttribute('#native-on', 'class') === 'yo' && + browser.getAttribute('#native-off', 'class') === '' && + browser.getAttribute('#native-force-on', 'class') == 'yo' && + browser.getAttribute('#native-force-off', 'class') == '' + ) { + browser.logger.info(`${agent} skipping .toggle`); + this.skip(); + } + }); + + it('adds class "yo"', function() { + const classStr = browser.getAttribute('#polyfilled-on', 'class'); + expect(classStr).to.equal('yo'); + }); + + it('removes class "yo"', function() { + const classStr = browser.getAttribute('#polyfilled-off', 'class'); + expect(classStr).to.equal(''); + }); + + it('retains class "yo" when force is `true`', function() { + const classStr = browser.getAttribute('#polyfilled-force-on', 'class'); + expect(classStr).to.equal('yo'); + }); + + it('does not add a class when force is `false`', function() { + const classStr = browser.getAttribute('#polyfilled-force-off', 'class'); + expect(classStr).to.equal(''); + }); + +}); + + +describe('.classList.toggle return', function() { + + before(function() { + browser.url('test/specs/toggle.html'); + const toggleOn = browser.execute('return window.nativeToggleOn').value; + const toggleOff = browser.execute('return window.nativeToggleOff').value; + const toggleForceOn = browser.execute('return window.nativeToggleForceOn').value; + const toggleForceOff = browser.execute('return window.nativeToggleForceOff').value; + + if( + toggleOn === true && + toggleOff === false && + toggleForceOn === true && + toggleForceOff === false + ) { + browser.logger.info(`${agent} skipping .toggle return`); + this.skip(); + } + }); + + it('is `true` when class is toggled on', function() { + const returnVal = browser.execute('return window.polyfilledToggleOn').value; + expect(returnVal).to.equal(true); + }); + + it('is `false` when class is toggled off', function() { + const returnVal = browser.execute('return window.polyfilledToggleOff').value; + expect(returnVal).to.equal(false); + }); + + it('is `true` when force is `true`', function() { + const returnVal = browser.execute('return window.polyfilledToggleForceOn').value; + expect(returnVal).to.equal(true); + }); + + it('is `false` when force is `false`', function() { + const returnVal = browser.execute('return window.polyfilledToggleForceOff').value; + expect(returnVal).to.equal(false); + }); + +}); diff --git a/test/wdio.defaults.js b/test/wdio.defaults.js new file mode 100644 index 0000000..033c6e5 --- /dev/null +++ b/test/wdio.defaults.js @@ -0,0 +1,52 @@ +const path = require('path'); +const chai = require('chai'); + +exports.getSauceCredentials = function() { + + let creds; + + try { + creds = require(path.join(__dirname, '..', 'user.json')); + } catch (e) { + creds = { + user: process.env.SAUCE_USERNAME, + key: process.env.SAUCE_ACCESS_KEY, + }; + } + + console.assert(creds.user, "Missing Sauce Labs username"); + console.assert(creds.key, "Missing Sauce Labs access key"); + + return creds; +}; + +exports.config = { + + before: function (capabilities, specs) { + global.expect = chai.expect; + const session = browser.session().value; + global.agent = `${session.browserName} ${session.browserVersion}`; + if (process.env.TRAVIS_BUILD_NUMBER) { + // enable saucelab badges + capabilities.public = true; + capabilities.build = process.env.TRAVIS_BUILD_NUMBER; + } + }, + + baseUrl: 'http://localhost:4567', + + logLevel: 'error', + + framework: 'mocha', + + reporters: ['spec'], + + specs: [ + path.join(__dirname, 'specs/**/*.js') + ], + + staticServerFolders: [ + { mount: '/', path: path.join(__dirname, '..') } + ], + +}; diff --git a/test/wdio.sample.js b/test/wdio.sample.js new file mode 100644 index 0000000..a4e4d1a --- /dev/null +++ b/test/wdio.sample.js @@ -0,0 +1,15 @@ +// can use any of the configs found in `/test` +const config = require('./test/wdio.defaults').config; + +exports.config = Object.assign(config, { + + capabilities: [ + { browserName: 'firefox', }, + ], + + services: [ + 'static-server', + 'selenium-standalone', + ], + +}); diff --git a/test/wdio.sauce.js b/test/wdio.sauce.js new file mode 100644 index 0000000..7300490 --- /dev/null +++ b/test/wdio.sauce.js @@ -0,0 +1,28 @@ +const path = require('path'); + +let defaults = require(path.join(__dirname, 'wdio.defaults')); + +const creds = defaults.getSauceCredentials(); + +exports.config = Object.assign(defaults.config, { + + // If a browser skips all tests then .classList is fully supported and + // it can be removed from this list. + // https://saucelabs.com/platforms + capabilities: [ + { browserName: 'internet explorer', version: 8, }, + { browserName: 'internet explorer', version: 9, }, + { browserName: 'internet explorer', version: 10, }, + { browserName: 'internet explorer', version: 11, }, + ], + + sauceConnect: true, + user: creds.user, + key: creds.key, + + services: [ + 'static-server', + 'sauce', + ], + +}); diff --git a/test/wdio.standalone.js b/test/wdio.standalone.js new file mode 100644 index 0000000..fd277ff --- /dev/null +++ b/test/wdio.standalone.js @@ -0,0 +1,16 @@ +const path = require('path'); + +let defaults = require(path.join(__dirname, 'wdio.defaults')); + +exports.config = Object.assign(defaults.config, { + + capabilities: [ + { browserName: 'firefox', }, + ], + + services: [ + 'static-server', + 'selenium-standalone', + ], + +}); diff --git a/tests/qunit.html b/tests/qunit.html deleted file mode 100644 index d80d3b4..0000000 --- a/tests/qunit.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - QUnit Tests - - - -
-
- - - - - - diff --git a/tests/remove.js b/tests/remove.js deleted file mode 100644 index 24cda33..0000000 --- a/tests/remove.js +++ /dev/null @@ -1,10 +0,0 @@ -QUnit.module("classList.remove"); - -QUnit.test("Removes duplicated instances of class", function(assert) { - var el = document.createElement("p"), cList = el.classList; - el.className = "ho ho ho" - - cList.remove("ho"); - assert.ok(!cList.contains("ho"), "Should remove all instances of 'ho'"); - assert.strictEqual(el.className, "") -}); diff --git a/tests/runner.coffee b/tests/runner.coffee deleted file mode 100644 index be9c56a..0000000 --- a/tests/runner.coffee +++ /dev/null @@ -1,46 +0,0 @@ -urls = require('system').args.slice(1) -page = require('webpage').create() -timeout = 3000 - -qunitHooks = -> - window.document.addEventListener 'DOMContentLoaded', -> - for callback in ['log', 'testDone', 'done'] - do (callback) -> - QUnit[callback] (result) -> - window.callPhantom - name: "QUnit.#{callback}" - data: result - -page.onInitialized = -> page.evaluate qunitHooks - -page.onConsoleMessage = (msg) -> console.log msg - -page.onCallback = (event) -> - if event.name is 'QUnit.log' - details = event.data - if details.result is false - console.log "✘ #{details.module}: #{details.name}" - if details.message and details.message isnt "failed" - console.log " #{details.message}" - if "actual" of details - console.log " expected: #{details.expected}" - console.log " actual: #{details.actual}" - else if event.name is 'QUnit.testDone' - result = event.data - unless result.failed - console.log "✔︎ #{result.module}: #{result.name}" - else if event.name is 'QUnit.done' - res = event.data - console.log "#{res.total} tests, #{res.failed} failed. Done in #{res.runtime} ms" - phantom.exit if !res.total or res.failed then 1 else 0 - -for url in urls - page.open url, (status) -> - if status isnt 'success' - console.error "failed opening #{url}: #{status}" - phantom.exit 1 - else - setTimeout -> - console.error "ERROR: Test execution has timed out" - phantom.exit 1 - , timeout diff --git a/tests/tests.js b/tests/tests.js deleted file mode 100644 index b76a72a..0000000 --- a/tests/tests.js +++ /dev/null @@ -1,82 +0,0 @@ -QUnit.module("classList.toggle"); - -QUnit.test("Adds a class", function(assert) { - var cList = document.createElement("p").classList; - - cList.toggle("c1"); - assert.ok(cList.contains("c1"), "Adds a class that is not present"); - - assert.strictEqual( - cList.toggle("c2"), - true, - "Returns true when class is added" - ); -}); - -QUnit.test("Removes a class", function(assert) { - var cList = document.createElement("p").classList; - - cList.add("c1"); - cList.toggle("c1"); - assert.ok(!cList.contains("c1"), "Removes a class that is present"); - - cList.add("c2"); - assert.strictEqual( - cList.toggle("c2"), - false, - "Return false when class is removed" - ); -}); - -QUnit.test("Adds class with second argument", function(assert) { - var cList = document.createElement("p").classList; - - cList.toggle("c1", true); - assert.ok(cList.contains("c1"), "Adds a class"); - - assert.strictEqual( - cList.toggle("c2", true), - true, - "Returns true when class is added" - ); - - cList.add("c3"); - cList.toggle("c3", true); - assert.ok( - cList.contains("c3"), - "Does not remove a class that is already present" - ); - - cList.add("c4"); - assert.strictEqual( - cList.toggle("c4", true), - true, - "Returns true when class is already present" - ); -}); - -QUnit.test("Removes class with second argument", function(assert) { - var cList = document.createElement("p").classList; - - cList.add("c1"); - cList.toggle("c1", false); - assert.ok(!cList.contains("c1"), "Removes a class"); - - assert.strictEqual( - cList.toggle("c2", false), - false, - "Returns false when class is removed" - ); - - cList.toggle("c3", false); - assert.ok( - !cList.contains("c3"), - "Does not add a class that is not present" - ); - - assert.strictEqual( - cList.toggle("c4", false), - false, - "Returns false when class was not present" - ); -});