[Pkg-javascript-commits] [node-negotiator] 05/09: New upstream version 0.6.1

Praveen Arimbrathodiyil praveen at moszumanska.debian.org
Mon Sep 4 16:36:23 UTC 2017


This is an automated email from the git hooks/post-receive script.

praveen pushed a commit to branch master
in repository node-negotiator.

commit f954fbdca8132a4c0e79d9adf1f87f64d9131dec
Author: Pirate Praveen <praveen at debian.org>
Date:   Mon Sep 4 21:45:46 2017 +0530

    New upstream version 0.6.1
---
 .gitignore        |  23 ++
 .travis.yml       |  29 ++-
 HISTORY.md        |  98 +++++++++
 LICENSE           |   3 +-
 README.md         |  88 ++++++--
 index.js          | 124 +++++++++++
 lib/charset.js    | 164 ++++++++++----
 lib/encoding.js   | 191 ++++++++++------
 lib/language.js   | 167 ++++++++++----
 lib/mediaType.js  | 290 ++++++++++++++++++++-----
 lib/negotiator.js |  37 ----
 package.json      |  38 ++--
 test/charset.js   | 408 ++++++++++++++++++++++++++++-------
 test/encoding.js  | 539 ++++++++++++++++++++++++++++++++++++---------
 test/language.js  | 531 +++++++++++++++++++++++++++++++++++----------
 test/mediaType.js | 635 ++++++++++++++++++++++++++++++++++++++++++------------
 16 files changed, 2649 insertions(+), 716 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3860288
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,23 @@
+# OS X
+.DS_Store*
+Icon?
+._*
+
+# Windows
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+
+# Linux
+.directory
+*~
+
+
+# npm
+node_modules
+*.log
+*.gz
+
+
+# Coveralls
+coverage
diff --git a/.travis.yml b/.travis.yml
index 25a64e6..f5f9911 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,23 @@
-node_js:
-- "0.6"
-- "0.8"
-- "0.10"
-- "0.11"
 language: node_js
-script: "npm run-script test-cov"
-after_script: "npm install coveralls at 2 && cat ./coverage/lcov.info | coveralls"
+node_js:
+  - "0.6"
+  - "0.8"
+  - "0.10"
+  - "0.12"
+  - "1.8"
+  - "2.5"
+  - "3.3"
+  - "4.4"
+  - "5.11"
+  - "6.0"
+sudo: false
+before_install:
+  # Setup Node.js version-specific dependencies
+  - "test $TRAVIS_NODE_VERSION != '0.6' || npm rm --save-dev istanbul"
+  - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul"
+script:
+  # Run test script, depending on istanbul install
+  - "test ! -z $(npm -ps ls istanbul) || npm test"
+  - "test   -z $(npm -ps ls istanbul) || npm run-script test-travis"
+after_script:
+  - "test -e ./coverage/lcov.info && npm install coveralls at 2 && cat ./coverage/lcov.info | coveralls"
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 0000000..10b6917
--- /dev/null
+++ b/HISTORY.md
@@ -0,0 +1,98 @@
+0.6.1 / 2016-05-02
+==================
+
+  * perf: improve `Accept` parsing speed
+  * perf: improve `Accept-Charset` parsing speed
+  * perf: improve `Accept-Encoding` parsing speed
+  * perf: improve `Accept-Language` parsing speed
+
+0.6.0 / 2015-09-29
+==================
+
+  * Fix including type extensions in parameters in `Accept` parsing
+  * Fix parsing `Accept` parameters with quoted equals
+  * Fix parsing `Accept` parameters with quoted semicolons
+  * Lazy-load modules from main entry point
+  * perf: delay type concatenation until needed
+  * perf: enable strict mode
+  * perf: hoist regular expressions
+  * perf: remove closures getting spec properties
+  * perf: remove a closure from media type parsing
+  * perf: remove property delete from media type parsing
+
+0.5.3 / 2015-05-10
+==================
+
+  * Fix media type parameter matching to be case-insensitive
+
+0.5.2 / 2015-05-06
+==================
+
+  * Fix comparing media types with quoted values
+  * Fix splitting media types with quoted commas
+
+0.5.1 / 2015-02-14
+==================
+
+  * Fix preference sorting to be stable for long acceptable lists
+
+0.5.0 / 2014-12-18
+==================
+
+  * Fix list return order when large accepted list
+  * Fix missing identity encoding when q=0 exists
+  * Remove dynamic building of Negotiator class
+
+0.4.9 / 2014-10-14
+==================
+
+  * Fix error when media type has invalid parameter
+
+0.4.8 / 2014-09-28
+==================
+
+  * Fix all negotiations to be case-insensitive
+  * Stable sort preferences of same quality according to client order
+  * Support Node.js 0.6
+
+0.4.7 / 2014-06-24
+==================
+
+  * Handle invalid provided languages
+  * Handle invalid provided media types
+
+0.4.6 / 2014-06-11
+==================
+
+  *  Order by specificity when quality is the same
+
+0.4.5 / 2014-05-29
+==================
+
+  * Fix regression in empty header handling
+
+0.4.4 / 2014-05-29
+==================
+
+  * Fix behaviors when headers are not present
+
+0.4.3 / 2014-04-16
+==================
+
+  * Handle slashes on media params correctly
+
+0.4.2 / 2014-02-28
+==================
+
+  * Fix media type sorting
+  * Handle media types params strictly
+
+0.4.1 / 2014-01-16
+==================
+
+  * Use most specific matches
+
+0.4.0 / 2014-01-09
+==================
+
+  * Remove preferred prefix from methods
diff --git a/LICENSE b/LICENSE
index 692b534..ea6b9e2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,8 @@
 (The MIT License)
 
-Copyright (c) 2012 Federico Romero
+Copyright (c) 2012-2014 Federico Romero
 Copyright (c) 2012-2014 Isaac Z. Schlueter
+Copyright (c) 2014-2015 Douglas Christopher Wilson
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
diff --git a/README.md b/README.md
index 2461eb1..04a67ff 100644
--- a/README.md
+++ b/README.md
@@ -44,20 +44,29 @@ You can check a working example at `examples/accept.js`.
 
 #### Methods
 
-##### mediaTypes(availableMediaTypes):
+##### mediaType()
 
-Returns an array of preferred media types ordered by priority from a list of available media types.
+Returns the most preferred media type from the client.
 
-##### mediaType(availableMediaType):
+##### mediaType(availableMediaType)
 
-Returns the top preferred media type from a list of available media types.
+Returns the most preferred media type from a list of available media types.
+
+##### mediaTypes()
+
+Returns an array of preferred media types ordered by the client preference.
+
+##### mediaTypes(availableMediaTypes)
+
+Returns an array of preferred media types ordered by priority from a list of
+available media types.
 
 ### Accept-Language Negotiation
 
 ```js
 negotiator = new Negotiator(request)
 
-availableLanguages = 'en', 'es', 'fr'
+availableLanguages = ['en', 'es', 'fr']
 
 // Let's say Accept-Language header is 'en;q=0.8, es, pt'
 
@@ -75,13 +84,22 @@ You can check a working example at `examples/language.js`.
 
 #### Methods
 
-##### languages(availableLanguages):
+##### language()
+
+Returns the most preferred language from the client.
 
-Returns an array of preferred languages ordered by priority from a list of available languages.
+##### language(availableLanguages)
 
-##### language(availableLanguages):
+Returns the most preferred language from a list of available languages.
 
-Returns the top preferred language from a list of available languages.
+##### languages()
+
+Returns an array of preferred languages ordered by the client preference.
+
+##### languages(availableLanguages)
+
+Returns an array of preferred languages ordered by priority from a list of
+available languages.
 
 ### Accept-Charset Negotiation
 
@@ -106,13 +124,22 @@ You can check a working example at `examples/charset.js`.
 
 #### Methods
 
-##### charsets(availableCharsets):
+##### charset()
+
+Returns the most preferred charset from the client.
 
-Returns an array of preferred charsets ordered by priority from a list of available charsets.
+##### charset(availableCharsets)
 
-##### charset(availableCharsets):
+Returns the most preferred charset from a list of available charsets.
 
-Returns the top preferred charset from a list of available charsets.
+##### charsets()
+
+Returns an array of preferred charsets ordered by the client preference.
+
+##### charsets(availableCharsets)
+
+Returns an array of preferred charsets ordered by priority from a list of
+available charsets.
 
 ### Accept-Encoding Negotiation
 
@@ -137,25 +164,40 @@ You can check a working example at `examples/encoding.js`.
 
 #### Methods
 
-##### encodings(availableEncodings):
+##### encoding()
+
+Returns the most preferred encoding from the client.
+
+##### encoding(availableEncodings)
+
+Returns the most preferred encoding from a list of available encodings.
+
+##### encodings()
+
+Returns an array of preferred encodings ordered by the client preference.
+
+##### encodings(availableEncodings)
 
-Returns an array of preferred encodings ordered by priority from a list of available encodings.
+Returns an array of preferred encodings ordered by priority from a list of
+available encodings.
 
-##### encoding(availableEncodings):
+## See Also
 
-Returns the top preferred encoding from a list of available encodings.
+The [accepts](https://npmjs.org/package/accepts#readme) module builds on
+this module and provides an alternative interface, mime type validation,
+and more.
 
 ## License
 
 [MIT](LICENSE)
 
-[npm-image]: https://img.shields.io/npm/v/negotiator.svg?style=flat
+[npm-image]: https://img.shields.io/npm/v/negotiator.svg
 [npm-url]: https://npmjs.org/package/negotiator
-[node-version-image]: https://img.shields.io/node/v/negotiator.svg?style=flat
-[node-version-url]: http://nodejs.org/download/
-[travis-image]: https://img.shields.io/travis/jshttp/negotiator.svg?style=flat
+[node-version-image]: https://img.shields.io/node/v/negotiator.svg
+[node-version-url]: https://nodejs.org/en/download/
+[travis-image]: https://img.shields.io/travis/jshttp/negotiator/master.svg
 [travis-url]: https://travis-ci.org/jshttp/negotiator
-[coveralls-image]: https://img.shields.io/coveralls/jshttp/negotiator.svg?style=flat
+[coveralls-image]: https://img.shields.io/coveralls/jshttp/negotiator/master.svg
 [coveralls-url]: https://coveralls.io/r/jshttp/negotiator?branch=master
-[downloads-image]: https://img.shields.io/npm/dm/negotiator.svg?style=flat
+[downloads-image]: https://img.shields.io/npm/dm/negotiator.svg
 [downloads-url]: https://npmjs.org/package/negotiator
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..8d4f6a2
--- /dev/null
+++ b/index.js
@@ -0,0 +1,124 @@
+/*!
+ * negotiator
+ * Copyright(c) 2012 Federico Romero
+ * Copyright(c) 2012-2014 Isaac Z. Schlueter
+ * Copyright(c) 2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Cached loaded submodules.
+ * @private
+ */
+
+var modules = Object.create(null);
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = Negotiator;
+module.exports.Negotiator = Negotiator;
+
+/**
+ * Create a Negotiator instance from a request.
+ * @param {object} request
+ * @public
+ */
+
+function Negotiator(request) {
+  if (!(this instanceof Negotiator)) {
+    return new Negotiator(request);
+  }
+
+  this.request = request;
+}
+
+Negotiator.prototype.charset = function charset(available) {
+  var set = this.charsets(available);
+  return set && set[0];
+};
+
+Negotiator.prototype.charsets = function charsets(available) {
+  var preferredCharsets = loadModule('charset').preferredCharsets;
+  return preferredCharsets(this.request.headers['accept-charset'], available);
+};
+
+Negotiator.prototype.encoding = function encoding(available) {
+  var set = this.encodings(available);
+  return set && set[0];
+};
+
+Negotiator.prototype.encodings = function encodings(available) {
+  var preferredEncodings = loadModule('encoding').preferredEncodings;
+  return preferredEncodings(this.request.headers['accept-encoding'], available);
+};
+
+Negotiator.prototype.language = function language(available) {
+  var set = this.languages(available);
+  return set && set[0];
+};
+
+Negotiator.prototype.languages = function languages(available) {
+  var preferredLanguages = loadModule('language').preferredLanguages;
+  return preferredLanguages(this.request.headers['accept-language'], available);
+};
+
+Negotiator.prototype.mediaType = function mediaType(available) {
+  var set = this.mediaTypes(available);
+  return set && set[0];
+};
+
+Negotiator.prototype.mediaTypes = function mediaTypes(available) {
+  var preferredMediaTypes = loadModule('mediaType').preferredMediaTypes;
+  return preferredMediaTypes(this.request.headers.accept, available);
+};
+
+// Backwards compatibility
+Negotiator.prototype.preferredCharset = Negotiator.prototype.charset;
+Negotiator.prototype.preferredCharsets = Negotiator.prototype.charsets;
+Negotiator.prototype.preferredEncoding = Negotiator.prototype.encoding;
+Negotiator.prototype.preferredEncodings = Negotiator.prototype.encodings;
+Negotiator.prototype.preferredLanguage = Negotiator.prototype.language;
+Negotiator.prototype.preferredLanguages = Negotiator.prototype.languages;
+Negotiator.prototype.preferredMediaType = Negotiator.prototype.mediaType;
+Negotiator.prototype.preferredMediaTypes = Negotiator.prototype.mediaTypes;
+
+/**
+ * Load the given module.
+ * @private
+ */
+
+function loadModule(moduleName) {
+  var module = modules[moduleName];
+
+  if (module !== undefined) {
+    return module;
+  }
+
+  // This uses a switch for static require analysis
+  switch (moduleName) {
+    case 'charset':
+      module = require('./lib/charset');
+      break;
+    case 'encoding':
+      module = require('./lib/encoding');
+      break;
+    case 'language':
+      module = require('./lib/language');
+      break;
+    case 'mediaType':
+      module = require('./lib/mediaType');
+      break;
+    default:
+      throw new Error('Cannot find module \'' + moduleName + '\'');
+  }
+
+  // Store to prevent invoking require()
+  modules[moduleName] = module;
+
+  return module;
+}
diff --git a/lib/charset.js b/lib/charset.js
index 58da58f..ac4217b 100644
--- a/lib/charset.js
+++ b/lib/charset.js
@@ -1,16 +1,57 @@
+/**
+ * negotiator
+ * Copyright(c) 2012 Isaac Z. Schlueter
+ * Copyright(c) 2014 Federico Romero
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module exports.
+ * @public
+ */
+
 module.exports = preferredCharsets;
-preferredCharsets.preferredCharsets = preferredCharsets;
+module.exports.preferredCharsets = preferredCharsets;
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var simpleCharsetRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/;
+
+/**
+ * Parse the Accept-Charset header.
+ * @private
+ */
 
 function parseAcceptCharset(accept) {
-  return accept.split(',').map(function(e, i) {
-    return parseCharset(e.trim(), i);
-  }).filter(function(e) {
-    return e;
-  });
+  var accepts = accept.split(',');
+
+  for (var i = 0, j = 0; i < accepts.length; i++) {
+    var charset = parseCharset(accepts[i].trim(), i);
+
+    if (charset) {
+      accepts[j++] = charset;
+    }
+  }
+
+  // trim accepts
+  accepts.length = j;
+
+  return accepts;
 }
 
-function parseCharset(s, i) {
-  var match = s.match(/^\s*(\S+?)\s*(?:;(.*))?$/);
+/**
+ * Parse a charset from the Accept-Charset header.
+ * @private
+ */
+
+function parseCharset(str, i) {
+  var match = simpleCharsetRegExp.exec(str);
   if (!match) return null;
 
   var charset = match[1];
@@ -33,19 +74,31 @@ function parseCharset(s, i) {
   };
 }
 
-function getCharsetPriority(charset, accepted) {
-  return (accepted.map(function(a) {
-    return specify(charset, a);
-  }).filter(Boolean).sort(function (a, b) {
-    if(a.s == b.s) {
-      return a.q > b.q ? -1 : 1;
-    } else {
-      return a.s > b.s ? -1 : 1;
+/**
+ * Get the priority of a charset.
+ * @private
+ */
+
+function getCharsetPriority(charset, accepted, index) {
+  var priority = {o: -1, q: 0, s: 0};
+
+  for (var i = 0; i < accepted.length; i++) {
+    var spec = specify(charset, accepted[i], index);
+
+    if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
+      priority = spec;
     }
-  })[0] || {s: 0, q:0});
+  }
+
+  return priority;
 }
 
-function specify(charset, spec) {
+/**
+ * Get the specificity of the charset.
+ * @private
+ */
+
+function specify(charset, spec, index) {
   var s = 0;
   if(spec.charset.toLowerCase() === charset.toLowerCase()){
     s |= 1;
@@ -54,34 +107,63 @@ function specify(charset, spec) {
   }
 
   return {
-    s: s,
+    i: index,
+    o: spec.i,
     q: spec.q,
+    s: s
   }
 }
 
+/**
+ * Get the preferred charsets from an Accept-Charset header.
+ * @public
+ */
+
 function preferredCharsets(accept, provided) {
   // RFC 2616 sec 14.2: no header = *
-  accept = parseAcceptCharset(accept === undefined ? '*' : accept || '');
-  if (provided) {
-    return provided.map(function(type) {
-      return [type, getCharsetPriority(type, accept)];
-    }).filter(function(pair) {
-      return pair[1].q > 0;
-    }).sort(function(a, b) {
-      var pa = a[1];
-      var pb = b[1];
-      return (pb.q - pa.q) || (pb.s - pa.s) || (pa.i - pb.i);
-    }).map(function(pair) {
-      return pair[0];
-    });
-  } else {
-    return accept.sort(function (a, b) {
-      // revsort
-      return (b.q - a.q) || (a.i - b.i);
-    }).filter(function(type) {
-      return type.q > 0;
-    }).map(function(type) {
-      return type.charset;
-    });
+  var accepts = parseAcceptCharset(accept === undefined ? '*' : accept || '');
+
+  if (!provided) {
+    // sorted list of all charsets
+    return accepts
+      .filter(isQuality)
+      .sort(compareSpecs)
+      .map(getFullCharset);
   }
+
+  var priorities = provided.map(function getPriority(type, index) {
+    return getCharsetPriority(type, accepts, index);
+  });
+
+  // sorted list of accepted charsets
+  return priorities.filter(isQuality).sort(compareSpecs).map(function getCharset(priority) {
+    return provided[priorities.indexOf(priority)];
+  });
+}
+
+/**
+ * Compare two specs.
+ * @private
+ */
+
+function compareSpecs(a, b) {
+  return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || (a.i - b.i) || 0;
+}
+
+/**
+ * Get full charset string.
+ * @private
+ */
+
+function getFullCharset(spec) {
+  return spec.charset;
+}
+
+/**
+ * Check if a spec has any quality.
+ * @private
+ */
+
+function isQuality(spec) {
+  return spec.q > 0;
 }
diff --git a/lib/encoding.js b/lib/encoding.js
index 4b8acc1..70ac3de 100644
--- a/lib/encoding.js
+++ b/lib/encoding.js
@@ -1,47 +1,73 @@
+/**
+ * negotiator
+ * Copyright(c) 2012 Isaac Z. Schlueter
+ * Copyright(c) 2014 Federico Romero
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module exports.
+ * @public
+ */
+
 module.exports = preferredEncodings;
-preferredEncodings.preferredEncodings = preferredEncodings;
+module.exports.preferredEncodings = preferredEncodings;
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var simpleEncodingRegExp = /^\s*([^\s;]+)\s*(?:;(.*))?$/;
+
+/**
+ * Parse the Accept-Encoding header.
+ * @private
+ */
 
 function parseAcceptEncoding(accept) {
-  var acceptableEncodings;
-
-  if (accept) {
-    acceptableEncodings = accept.split(',').map(function(e, i) {
-      return parseEncoding(e.trim(), i);
-    });
-  } else {
-    acceptableEncodings = [];
+  var accepts = accept.split(',');
+  var hasIdentity = false;
+  var minQuality = 1;
+
+  for (var i = 0, j = 0; i < accepts.length; i++) {
+    var encoding = parseEncoding(accepts[i].trim(), i);
+
+    if (encoding) {
+      accepts[j++] = encoding;
+      hasIdentity = hasIdentity || specify('identity', encoding);
+      minQuality = Math.min(minQuality, encoding.q || 1);
+    }
   }
 
-  if (!acceptableEncodings.some(function(e) {
-    return e && specify('identity', e);
-  })) {
+  if (!hasIdentity) {
     /*
      * If identity doesn't explicitly appear in the accept-encoding header,
      * it's added to the list of acceptable encoding with the lowest q
-     *
      */
-    var lowestQ = 1;
-
-    for(var i = 0; i < acceptableEncodings.length; i++){
-      var e = acceptableEncodings[i];
-      if(e && e.q < lowestQ){
-        lowestQ = e.q;
-      }
-    }
-    acceptableEncodings.push({
+    accepts[j++] = {
       encoding: 'identity',
-      q: lowestQ / 2,
-    });
+      q: minQuality,
+      i: i
+    };
   }
 
-  return acceptableEncodings.filter(function(e) {
-    return e;
-  });
+  // trim accepts
+  accepts.length = j;
+
+  return accepts;
 }
 
-function parseEncoding(s, i) {
-  var match = s.match(/^\s*(\S+?)\s*(?:;(.*))?$/);
+/**
+ * Parse an encoding from the Accept-Encoding header.
+ * @private
+ */
 
+function parseEncoding(str, i) {
+  var match = simpleEncodingRegExp.exec(str);
   if (!match) return null;
 
   var encoding = match[1];
@@ -64,19 +90,31 @@ function parseEncoding(s, i) {
   };
 }
 
-function getEncodingPriority(encoding, accepted) {
-  return (accepted.map(function(a) {
-    return specify(encoding, a);
-  }).filter(Boolean).sort(function (a, b) {
-    if(a.s == b.s) {
-      return a.q > b.q ? -1 : 1;
-    } else {
-      return a.s > b.s ? -1 : 1;
+/**
+ * Get the priority of an encoding.
+ * @private
+ */
+
+function getEncodingPriority(encoding, accepted, index) {
+  var priority = {o: -1, q: 0, s: 0};
+
+  for (var i = 0; i < accepted.length; i++) {
+    var spec = specify(encoding, accepted[i], index);
+
+    if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
+      priority = spec;
     }
-  })[0] || {s: 0, q: 0});
+  }
+
+  return priority;
 }
 
-function specify(encoding, spec) {
+/**
+ * Get the specificity of the encoding.
+ * @private
+ */
+
+function specify(encoding, spec, index) {
   var s = 0;
   if(spec.encoding.toLowerCase() === encoding.toLowerCase()){
     s |= 1;
@@ -85,33 +123,62 @@ function specify(encoding, spec) {
   }
 
   return {
-    s: s,
+    i: index,
+    o: spec.i,
     q: spec.q,
+    s: s
   }
 };
 
+/**
+ * Get the preferred encodings from an Accept-Encoding header.
+ * @public
+ */
+
 function preferredEncodings(accept, provided) {
-  accept = parseAcceptEncoding(accept || '');
-  if (provided) {
-    return provided.map(function(type) {
-      return [type, getEncodingPriority(type, accept)];
-    }).filter(function(pair) {
-      return pair[1].q > 0;
-    }).sort(function(a, b) {
-      var pa = a[1];
-      var pb = b[1];
-      return (pb.q - pa.q) || (pb.s - pa.s) || (pa.i - pb.i);
-    }).map(function(pair) {
-      return pair[0];
-    });
-  } else {
-    return accept.sort(function (a, b) {
-      // revsort
-      return (b.q - a.q) || (a.i - b.i);
-    }).filter(function(type){
-      return type.q > 0;
-    }).map(function(type) {
-      return type.encoding;
-    });
+  var accepts = parseAcceptEncoding(accept || '');
+
+  if (!provided) {
+    // sorted list of all encodings
+    return accepts
+      .filter(isQuality)
+      .sort(compareSpecs)
+      .map(getFullEncoding);
   }
+
+  var priorities = provided.map(function getPriority(type, index) {
+    return getEncodingPriority(type, accepts, index);
+  });
+
+  // sorted list of accepted encodings
+  return priorities.filter(isQuality).sort(compareSpecs).map(function getEncoding(priority) {
+    return provided[priorities.indexOf(priority)];
+  });
+}
+
+/**
+ * Compare two specs.
+ * @private
+ */
+
+function compareSpecs(a, b) {
+  return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || (a.i - b.i) || 0;
+}
+
+/**
+ * Get full encoding string.
+ * @private
+ */
+
+function getFullEncoding(spec) {
+  return spec.encoding;
+}
+
+/**
+ * Check if a spec has any quality.
+ * @private
+ */
+
+function isQuality(spec) {
+  return spec.q > 0;
 }
diff --git a/lib/language.js b/lib/language.js
index 8fc63df..1bd2d0e 100644
--- a/lib/language.js
+++ b/lib/language.js
@@ -1,16 +1,57 @@
+/**
+ * negotiator
+ * Copyright(c) 2012 Isaac Z. Schlueter
+ * Copyright(c) 2014 Federico Romero
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module exports.
+ * @public
+ */
+
 module.exports = preferredLanguages;
-preferredLanguages.preferredLanguages = preferredLanguages;
+module.exports.preferredLanguages = preferredLanguages;
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var simpleLanguageRegExp = /^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$/;
+
+/**
+ * Parse the Accept-Language header.
+ * @private
+ */
 
 function parseAcceptLanguage(accept) {
-  return accept.split(',').map(function(e, i) {
-    return parseLanguage(e.trim(), i);
-  }).filter(function(e) {
-    return e;
-  });
+  var accepts = accept.split(',');
+
+  for (var i = 0, j = 0; i < accepts.length; i++) {
+    var langauge = parseLanguage(accepts[i].trim(), i);
+
+    if (langauge) {
+      accepts[j++] = langauge;
+    }
+  }
+
+  // trim accepts
+  accepts.length = j;
+
+  return accepts;
 }
 
-function parseLanguage(s, i) {
-  var match = s.match(/^\s*(\S+?)(?:-(\S+?))?\s*(?:;(.*))?$/);
+/**
+ * Parse a language from the Accept-Language header.
+ * @private
+ */
+
+function parseLanguage(str, i) {
+  var match = simpleLanguageRegExp.exec(str);
   if (!match) return null;
 
   var prefix = match[1],
@@ -37,19 +78,31 @@ function parseLanguage(s, i) {
   };
 }
 
-function getLanguagePriority(language, accepted) {
-  return (accepted.map(function(a){
-    return specify(language, a);
-  }).filter(Boolean).sort(function (a, b) {
-    if(a.s == b.s) {
-      return a.q > b.q ? -1 : 1;
-    } else {
-      return a.s > b.s ? -1 : 1;
+/**
+ * Get the priority of a language.
+ * @private
+ */
+
+function getLanguagePriority(language, accepted, index) {
+  var priority = {o: -1, q: 0, s: 0};
+
+  for (var i = 0; i < accepted.length; i++) {
+    var spec = specify(language, accepted[i], index);
+
+    if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
+      priority = spec;
     }
-  })[0] || {s: 0, q: 0});
+  }
+
+  return priority;
 }
 
-function specify(language, spec) {
+/**
+ * Get the specificity of the language.
+ * @private
+ */
+
+function specify(language, spec, index) {
   var p = parseLanguage(language)
   if (!p) return null;
   var s = 0;
@@ -64,37 +117,63 @@ function specify(language, spec) {
   }
 
   return {
-    s: s,
+    i: index,
+    o: spec.i,
     q: spec.q,
+    s: s
   }
 };
 
+/**
+ * Get the preferred languages from an Accept-Language header.
+ * @public
+ */
+
 function preferredLanguages(accept, provided) {
   // RFC 2616 sec 14.4: no header = *
-  accept = parseAcceptLanguage(accept === undefined ? '*' : accept || '');
-  if (provided) {
-
-    var ret = provided.map(function(type) {
-      return [type, getLanguagePriority(type, accept)];
-    }).filter(function(pair) {
-      return pair[1].q > 0;
-    }).sort(function(a, b) {
-      var pa = a[1];
-      var pb = b[1];
-      return (pb.q - pa.q) || (pb.s - pa.s) || (pa.i - pb.i);
-    }).map(function(pair) {
-      return pair[0];
-    });
-    return ret;
-
-  } else {
-    return accept.sort(function (a, b) {
-      // revsort
-      return (b.q - a.q) || (a.i - b.i);
-    }).filter(function(type) {
-      return type.q > 0;
-    }).map(function(type) {
-      return type.full;
-    });
+  var accepts = parseAcceptLanguage(accept === undefined ? '*' : accept || '');
+
+  if (!provided) {
+    // sorted list of all languages
+    return accepts
+      .filter(isQuality)
+      .sort(compareSpecs)
+      .map(getFullLanguage);
   }
+
+  var priorities = provided.map(function getPriority(type, index) {
+    return getLanguagePriority(type, accepts, index);
+  });
+
+  // sorted list of accepted languages
+  return priorities.filter(isQuality).sort(compareSpecs).map(function getLanguage(priority) {
+    return provided[priorities.indexOf(priority)];
+  });
+}
+
+/**
+ * Compare two specs.
+ * @private
+ */
+
+function compareSpecs(a, b) {
+  return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || (a.i - b.i) || 0;
+}
+
+/**
+ * Get full language string.
+ * @private
+ */
+
+function getFullLanguage(spec) {
+  return spec.full;
+}
+
+/**
+ * Check if a spec has any quality.
+ * @private
+ */
+
+function isQuality(spec) {
+  return spec.q > 0;
 }
diff --git a/lib/mediaType.js b/lib/mediaType.js
index 55e74a1..67309dd 100644
--- a/lib/mediaType.js
+++ b/lib/mediaType.js
@@ -1,35 +1,84 @@
+/**
+ * negotiator
+ * Copyright(c) 2012 Isaac Z. Schlueter
+ * Copyright(c) 2014 Federico Romero
+ * Copyright(c) 2014-2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict';
+
+/**
+ * Module exports.
+ * @public
+ */
+
 module.exports = preferredMediaTypes;
-preferredMediaTypes.preferredMediaTypes = preferredMediaTypes;
+module.exports.preferredMediaTypes = preferredMediaTypes;
+
+/**
+ * Module variables.
+ * @private
+ */
+
+var simpleMediaTypeRegExp = /^\s*([^\s\/;]+)\/([^;\s]+)\s*(?:;(.*))?$/;
+
+/**
+ * Parse the Accept header.
+ * @private
+ */
 
 function parseAccept(accept) {
-  return accept.split(',').map(function(e, i) {
-    return parseMediaType(e.trim(), i);
-  }).filter(function(e) {
-    return e;
-  });
-};
+  var accepts = splitMediaTypes(accept);
+
+  for (var i = 0, j = 0; i < accepts.length; i++) {
+    var mediaType = parseMediaType(accepts[i].trim(), i);
 
-function parseMediaType(s, i) {
-  var match = s.match(/\s*(\S+?)\/([^;\s]+)\s*(?:;(.*))?/);
+    if (mediaType) {
+      accepts[j++] = mediaType;
+    }
+  }
+
+  // trim accepts
+  accepts.length = j;
+
+  return accepts;
+}
+
+/**
+ * Parse a media type from the Accept header.
+ * @private
+ */
+
+function parseMediaType(str, i) {
+  var match = simpleMediaTypeRegExp.exec(str);
   if (!match) return null;
 
-  var type = match[1],
-      subtype = match[2],
-      full = "" + type + "/" + subtype,
-      params = {},
-      q = 1;
+  var params = Object.create(null);
+  var q = 1;
+  var subtype = match[2];
+  var type = match[1];
 
   if (match[3]) {
-    params = match[3].split(';').map(function(s) {
-      return s.trim().split('=');
-    }).reduce(function (set, p) {
-      set[p[0]] = p[1];
-      return set
-    }, params);
-
-    if (params.q != null) {
-      q = parseFloat(params.q);
-      delete params.q;
+    var kvps = splitParameters(match[3]).map(splitKeyValuePair);
+
+    for (var j = 0; j < kvps.length; j++) {
+      var pair = kvps[j];
+      var key = pair[0].toLowerCase();
+      var val = pair[1];
+
+      // get the value, unwrapping quotes
+      var value = val && val[0] === '"' && val[val.length - 1] === '"'
+        ? val.substr(1, val.length - 2)
+        : val;
+
+      if (key === 'q') {
+        q = parseFloat(value);
+        break;
+      }
+
+      // store parameter
+      params[key] = value;
     }
   }
 
@@ -38,24 +87,35 @@ function parseMediaType(s, i) {
     subtype: subtype,
     params: params,
     q: q,
-    i: i,
-    full: full
+    i: i
   };
 }
 
-function getMediaTypePriority(type, accepted) {
-  return (accepted.map(function(a) {
-    return specify(type, a);
-  }).filter(Boolean).sort(function (a, b) {
-    if(a.s == b.s) {
-      return a.q > b.q ? -1 : 1;
-    } else {
-      return a.s > b.s ? -1 : 1;
+/**
+ * Get the priority of a media type.
+ * @private
+ */
+
+function getMediaTypePriority(type, accepted, index) {
+  var priority = {o: -1, q: 0, s: 0};
+
+  for (var i = 0; i < accepted.length; i++) {
+    var spec = specify(type, accepted[i], index);
+
+    if (spec && (priority.s - spec.s || priority.q - spec.q || priority.o - spec.o) < 0) {
+      priority = spec;
     }
-  })[0] || {s: 0, q: 0});
+  }
+
+  return priority;
 }
 
-function specify(type, spec) {
+/**
+ * Get the specificity of the media type.
+ * @private
+ */
+
+function specify(type, spec, index) {
   var p = parseMediaType(type);
   var s = 0;
 
@@ -78,7 +138,7 @@ function specify(type, spec) {
   var keys = Object.keys(spec.params);
   if (keys.length > 0) {
     if (keys.every(function (k) {
-      return spec.params[k] == '*' || spec.params[k].toLowerCase() == (p.params[k] || '').toLowerCase();
+      return spec.params[k] == '*' || (spec.params[k] || '').toLowerCase() == (p.params[k] || '').toLowerCase();
     })) {
       s |= 1
     } else {
@@ -87,36 +147,148 @@ function specify(type, spec) {
   }
 
   return {
+    i: index,
+    o: spec.i,
     q: spec.q,
     s: s,
   }
-
 }
 
+/**
+ * Get the preferred media types from an Accept header.
+ * @public
+ */
+
 function preferredMediaTypes(accept, provided) {
   // RFC 2616 sec 14.2: no header = */*
-  accept = parseAccept(accept === undefined ? '*/*' : accept || '');
-  if (provided) {
-    return provided.map(function(type) {
-      return [type, getMediaTypePriority(type, accept)];
-    }).filter(function(pair) {
-      return pair[1].q > 0;
-    }).sort(function(a, b) {
-      var pa = a[1];
-      var pb = b[1];
-      return (pb.q - pa.q) || (pb.s - pa.s) || (pa.i - pb.i);
-    }).map(function(pair) {
-      return pair[0];
-    });
+  var accepts = parseAccept(accept === undefined ? '*/*' : accept || '');
 
+  if (!provided) {
+    // sorted list of all types
+    return accepts
+      .filter(isQuality)
+      .sort(compareSpecs)
+      .map(getFullType);
+  }
+
+  var priorities = provided.map(function getPriority(type, index) {
+    return getMediaTypePriority(type, accepts, index);
+  });
+
+  // sorted list of accepted types
+  return priorities.filter(isQuality).sort(compareSpecs).map(function getType(priority) {
+    return provided[priorities.indexOf(priority)];
+  });
+}
+
+/**
+ * Compare two specs.
+ * @private
+ */
+
+function compareSpecs(a, b) {
+  return (b.q - a.q) || (b.s - a.s) || (a.o - b.o) || (a.i - b.i) || 0;
+}
+
+/**
+ * Get full type string.
+ * @private
+ */
+
+function getFullType(spec) {
+  return spec.type + '/' + spec.subtype;
+}
+
+/**
+ * Check if a spec has any quality.
+ * @private
+ */
+
+function isQuality(spec) {
+  return spec.q > 0;
+}
+
+/**
+ * Count the number of quotes in a string.
+ * @private
+ */
+
+function quoteCount(string) {
+  var count = 0;
+  var index = 0;
+
+  while ((index = string.indexOf('"', index)) !== -1) {
+    count++;
+    index++;
+  }
+
+  return count;
+}
+
+/**
+ * Split a key value pair.
+ * @private
+ */
+
+function splitKeyValuePair(str) {
+  var index = str.indexOf('=');
+  var key;
+  var val;
+
+  if (index === -1) {
+    key = str;
   } else {
-    return accept.sort(function (a, b) {
-      // revsort
-      return (b.q - a.q) || (a.i - b.i);
-    }).filter(function(type) {
-      return type.q > 0;
-    }).map(function(type) {
-      return type.full;
-    });
+    key = str.substr(0, index);
+    val = str.substr(index + 1);
+  }
+
+  return [key, val];
+}
+
+/**
+ * Split an Accept header into media types.
+ * @private
+ */
+
+function splitMediaTypes(accept) {
+  var accepts = accept.split(',');
+
+  for (var i = 1, j = 0; i < accepts.length; i++) {
+    if (quoteCount(accepts[j]) % 2 == 0) {
+      accepts[++j] = accepts[i];
+    } else {
+      accepts[j] += ',' + accepts[i];
+    }
+  }
+
+  // trim accepts
+  accepts.length = j + 1;
+
+  return accepts;
+}
+
+/**
+ * Split a string of parameters.
+ * @private
+ */
+
+function splitParameters(str) {
+  var parameters = str.split(';');
+
+  for (var i = 1, j = 0; i < parameters.length; i++) {
+    if (quoteCount(parameters[j]) % 2 == 0) {
+      parameters[++j] = parameters[i];
+    } else {
+      parameters[j] += ';' + parameters[i];
+    }
   }
+
+  // trim parameters
+  parameters.length = j + 1;
+
+  for (var i = 0; i < parameters.length; i++) {
+    parameters[i] = parameters[i].trim();
+  }
+
+  return parameters;
 }
diff --git a/lib/negotiator.js b/lib/negotiator.js
deleted file mode 100644
index ba0c48b..0000000
--- a/lib/negotiator.js
+++ /dev/null
@@ -1,37 +0,0 @@
-module.exports = Negotiator;
-Negotiator.Negotiator = Negotiator;
-
-function Negotiator(request) {
-  if (!(this instanceof Negotiator)) return new Negotiator(request);
-  this.request = request;
-}
-
-var set = { charset: 'accept-charset',
-            encoding: 'accept-encoding',
-            language: 'accept-language',
-            mediaType: 'accept' };
-
-
-function capitalize(string){
-  return string.charAt(0).toUpperCase() + string.slice(1);
-}
-
-Object.keys(set).forEach(function (k) {
-  var header = set[k],
-      method = require('./'+k+'.js'),
-      singular = k,
-      plural = k + 's';
-
-  Negotiator.prototype[plural] = function (available) {
-    return method(this.request.headers[header], available);
-  };
-
-  Negotiator.prototype[singular] = function(available) {
-    var set = this[plural](available);
-    if (set) return set[0];
-  };
-
-  // Keep preferred* methods for legacy compatibility
-  Negotiator.prototype['preferred'+capitalize(plural)] = Negotiator.prototype[plural];
-  Negotiator.prototype['preferred'+capitalize(singular)] = Negotiator.prototype[singular];
-})
diff --git a/package.json b/package.json
index f63c4b6..6c5d066 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,13 @@
 {
   "name": "negotiator",
   "description": "HTTP content negotiation",
-  "version": "0.4.8",
-  "author": "Federico Romero <federico.romero at outboxlabs.com>",
-  "contributors": ["Isaac Z. Schlueter <i at izs.me> (http://blog.izs.me/)"],
-  "repository": "jshttp/negotiator",
+  "version": "0.6.1",
+  "contributors": [
+    "Douglas Christopher Wilson <doug at somethingdoug.com>",
+    "Federico Romero <federico.romero at outboxlabs.com>",
+    "Isaac Z. Schlueter <i at izs.me> (http://blog.izs.me/)"
+  ],
+  "license": "MIT",
   "keywords": [
     "http",
     "content negotiation",
@@ -13,21 +16,24 @@
     "accept-encoding",
     "accept-charset"
   ],
-  "license": "MIT",
+  "repository": "jshttp/negotiator",
   "devDependencies": {
-    "istanbul": "~0.3.2",
-    "nodeunit": "0.8.x"
-  },
-  "scripts": {
-    "test": "nodeunit test",
-    "test-cov": "istanbul cover ./node_modules/nodeunit/bin/nodeunit test"
+    "istanbul": "0.4.3",
+    "mocha": "~1.21.5"
   },
+  "files": [
+    "lib/",
+    "HISTORY.md",
+    "LICENSE",
+    "index.js",
+    "README.md"
+  ],
   "engines": {
     "node": ">= 0.6"
   },
-  "main": "lib/negotiator.js",
-  "files": [
-    "lib",
-    "LICENSE"
-  ]
+  "scripts": {
+    "test": "mocha --reporter spec --check-leaks --bail test/",
+    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/",
+    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/"
+  }
 }
diff --git a/test/charset.js b/test/charset.js
index c365898..5efa155 100644
--- a/test/charset.js
+++ b/test/charset.js
@@ -1,83 +1,329 @@
-(function() {
-  var configuration, preferredCharsets, testConfigurations, testCorrectCharset, _i, _len,
-    _this = this;
-
-  preferredCharsets = require('../lib/charset').preferredCharsets;
-
-  this["Should not return a charset when no charset is provided"] = function(test) {
-    test.deepEqual(preferredCharsets('*', []), []);
-    return test.done();
-  };
-
-  this["Should not return a charset when no charset is acceptable"] = function(test) {
-    test.deepEqual(preferredCharsets('ISO-8859-1', ['utf-8']), []);
-    return test.done();
-  };
-
-  this["Should not return a charset with q = 0"] = function(test) {
-    test.deepEqual(preferredCharsets('utf-8;q=0', ['utf-8']), []);
-    return test.done();
-  };
-
-  this["Should be case insensitive"] = function(test) {
-    test.deepEqual(preferredCharsets('iso-8859-1', ['ISO-8859-1']), ['ISO-8859-1']);
-    return test.done();
-  };
-
-  testCorrectCharset = function(c) {
-    return _this["Should return " + c.selected + " for accept-charset header " + c.accept + " with provided charset " + c.provided] = function(test) {
-      test.deepEqual(preferredCharsets(c.accept, c.provided), c.selected);
-      return test.done();
-    };
-  };
-
-  testConfigurations = [
-    {
-      accept: undefined,
-      provided: ['utf-8'],
-      selected: ['utf-8']
-    }, {
-      accept: 'utf-8',
-      provided: ['utf-8'],
-      selected: ['utf-8']
-    }, {
-      accept: '*',
-      provided: ['utf-8'],
-      selected: ['utf-8']
-    }, {
-      accept: 'utf-8',
-      provided: ['utf-8', 'ISO-8859-1'],
-      selected: ['utf-8']
-    }, {
-      accept: 'utf-8, ISO-8859-1',
-      provided: ['utf-8'],
-      selected: ['utf-8']
-    }, {
-      accept: 'utf-8;q=0.8, ISO-8859-1',
-      provided: ['utf-8', 'ISO-8859-1'],
-      selected: ['ISO-8859-1', 'utf-8']
-    }, {
-      accept: 'utf-8;q=0.8, ISO-8859-1',
-      provided: null,
-      selected: ['ISO-8859-1', 'utf-8']
-    }, {
-      accept: '*, utf-8;q=0',
-      provided: ['utf-8', 'ISO-8859-1'],
-      selected: ['ISO-8859-1']
-    }, {
-      accept : '*, utf-8',
-      provided: ['utf-8', 'ISO-8859-1' ],
-      selected: ['utf-8', 'ISO-8859-1' ]
-    }, {
-      accept : 'utf-8;q=0.9, ISO-8859-1;q=0.8, utf-8;q=0.7',
-      provided: ['utf-8', 'ISO-8859-1' ],
-      selected: ['utf-8', 'ISO-8859-1' ]
-    }
-  ];
-
-  for (_i = 0, _len = testConfigurations.length; _i < _len; _i++) {
-    configuration = testConfigurations[_i];
-    testCorrectCharset(configuration);
+
+var assert = require('assert')
+var Negotiator = require('..')
+
+describe('negotiator.charset()', function () {
+  whenAcceptCharset(undefined, function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.charset(), '*')
+    })
+  })
+
+  whenAcceptCharset('*', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.charset(), '*')
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.charset(), '*')
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8;q=0', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.charset(), '*')
+    })
+  })
+
+  whenAcceptCharset('ISO-8859-1', function () {
+    it('should return ISO-8859-1', function () {
+      assert.strictEqual(this.negotiator.charset(), 'ISO-8859-1')
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0', function () {
+    it('should return undefined', function () {
+      assert.strictEqual(this.negotiator.charset(), undefined)
+    })
+  })
+
+  whenAcceptCharset('UTF-8, ISO-8859-1', function () {
+    it('should return UTF-8', function () {
+      assert.strictEqual(this.negotiator.charset(), 'UTF-8')
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.8, ISO-8859-1', function () {
+    it('should return ISO-8859-1', function () {
+      assert.strictEqual(this.negotiator.charset(), 'ISO-8859-1')
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.9, ISO-8859-1;q=0.8, UTF-8;q=0.7', function () {
+    it('should return UTF-8', function () {
+      assert.strictEqual(this.negotiator.charset(), 'UTF-8')
+    })
+  })
+})
+
+describe('negotiator.charset(array)', function () {
+  whenAcceptCharset(undefined, function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.charset([]), undefined)
+    })
+
+    it('should return first type in list', function () {
+      assert.strictEqual(this.negotiator.charset(['UTF-8']), 'UTF-8')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'ISO-8859-1']), 'UTF-8')
+    })
+  })
+
+  whenAcceptCharset('*', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.charset([]), undefined)
+    })
+
+    it('should return first type in list', function () {
+      assert.strictEqual(this.negotiator.charset(['UTF-8']), 'UTF-8')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'ISO-8859-1']), 'UTF-8')
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8', function () {
+    it('should return first type in list', function () {
+      assert.strictEqual(this.negotiator.charset(['UTF-8']), 'UTF-8')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'ISO-8859-1']), 'UTF-8')
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8;q=0', function () {
+    it('should return most client-preferred charset', function () {
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'ISO-8859-1']), 'ISO-8859-1')
+    })
+
+    it('should exclude UTF-8', function () {
+      assert.strictEqual(this.negotiator.charset(['UTF-8']), undefined)
+    })
+  })
+
+  whenAcceptCharset('ISO-8859-1', function () {
+    it('should return matching charset', function () {
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1']), 'ISO-8859-1')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'ISO-8859-1']), 'ISO-8859-1')
+    })
+
+    it('should be case insensitive, returning provided casing', function () {
+      assert.strictEqual(this.negotiator.charset(['iso-8859-1']), 'iso-8859-1')
+      assert.strictEqual(this.negotiator.charset(['iso-8859-1', 'ISO-8859-1']), 'iso-8859-1')
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1', 'iso-8859-1']), 'ISO-8859-1')
+    })
+
+    it('should return undefined when no matching charsets', function () {
+      assert.strictEqual(this.negotiator.charset(['utf-8']), undefined)
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0', function () {
+    it('should always return undefined', function () {
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1']), undefined)
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'KOI8-R', 'ISO-8859-1']), undefined)
+      assert.strictEqual(this.negotiator.charset(['KOI8-R']), undefined)
+    })
+  })
+
+  whenAcceptCharset('UTF-8, ISO-8859-1', function () {
+    it('should return first matching charset', function () {
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1']), 'ISO-8859-1')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'KOI8-R', 'ISO-8859-1']), 'UTF-8')
+    })
+
+    it('should return undefined when no matching charsets', function () {
+      assert.strictEqual(this.negotiator.charset(['KOI8-R']), undefined)
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.8, ISO-8859-1', function () {
+    it('should return most client-preferred charset', function () {
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1']), 'ISO-8859-1')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'KOI8-R', 'ISO-8859-1']), 'ISO-8859-1')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'KOI8-R']), 'UTF-8')
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.9, ISO-8859-1;q=0.8, UTF-8;q=0.7', function () {
+    it('should use highest perferred order on duplicate', function () {
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1']), 'ISO-8859-1')
+      assert.strictEqual(this.negotiator.charset(['UTF-8', 'ISO-8859-1']), 'UTF-8')
+      assert.strictEqual(this.negotiator.charset(['ISO-8859-1', 'UTF-8']), 'UTF-8')
+    })
+  })
+})
+
+describe('negotiator.charsets()', function () {
+  whenAcceptCharset(undefined, function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['*'])
+    })
+  })
+
+  whenAcceptCharset('*', function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['*'])
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8', function () {
+    it('should return client-preferred charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['*', 'UTF-8'])
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8;q=0', function () {
+    it('should exclude UTF-8', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['*'])
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0', function () {
+    it('should return empty list', function () {
+      assert.deepEqual(this.negotiator.charsets(), [])
+    })
+  })
+
+  whenAcceptCharset('ISO-8859-1', function () {
+    it('should return client-preferred charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['ISO-8859-1'])
+    })
+  })
+
+  whenAcceptCharset('UTF-8, ISO-8859-1', function () {
+    it('should return client-preferred charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['UTF-8', 'ISO-8859-1'])
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.8, ISO-8859-1', function () {
+    it('should return client-preferred charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['ISO-8859-1', 'UTF-8'])
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.9, ISO-8859-1;q=0.8, UTF-8;q=0.7', function () {
+    it.skip('should use highest perferred order on duplicate', function () {
+      assert.deepEqual(this.negotiator.charsets(), ['UTF-8', 'ISO-8859-1'])
+    })
+  })
+})
+
+describe('negotiator.charsets(array)', function () {
+  whenAcceptCharset(undefined, function () {
+    it('should return empty list for empty list', function () {
+      assert.deepEqual(this.negotiator.charsets([]), [])
+    })
+
+    it('should return original list', function () {
+      assert.deepEqual(this.negotiator.charsets(['UTF-8']), ['UTF-8'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'ISO-8859-1']), ['UTF-8', 'ISO-8859-1'])
+    })
+  })
+
+  whenAcceptCharset('*', function () {
+    it('should return empty list for empty list', function () {
+      assert.deepEqual(this.negotiator.charsets([]), [])
+    })
+
+    it('should return original list', function () {
+      assert.deepEqual(this.negotiator.charsets(['UTF-8']), ['UTF-8'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'ISO-8859-1']), ['UTF-8', 'ISO-8859-1'])
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8', function () {
+    it('should return matching charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(['UTF-8']), ['UTF-8'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'ISO-8859-1']), ['UTF-8', 'ISO-8859-1'])
+    })
+  })
+
+  whenAcceptCharset('*, UTF-8;q=0', function () {
+    it('should exclude UTF-8', function () {
+      assert.deepEqual(this.negotiator.charsets(['UTF-8']), [])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'ISO-8859-1']), ['ISO-8859-1'])
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0', function () {
+    it('should always return empty list', function () {
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1']), [])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'KOI8-R', 'ISO-8859-1']), [])
+      assert.deepEqual(this.negotiator.charsets(['KOI8-R']), [])
+    })
+  })
+
+  whenAcceptCharset('ISO-8859-1', function () {
+    it('should return matching charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1']), ['ISO-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'ISO-8859-1']), ['ISO-8859-1'])
+    })
+
+    it('should be case insensitive, returning provided casing', function () {
+      assert.deepEqual(this.negotiator.charsets(['iso-8859-1']), ['iso-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['iso-8859-1', 'ISO-8859-1']), ['iso-8859-1', 'ISO-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1', 'iso-8859-1']), ['ISO-8859-1', 'iso-8859-1'])
+    })
+
+    it('should return empty list when no matching charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(['utf-8']), [])
+    })
+  })
+
+  whenAcceptCharset('UTF-8, ISO-8859-1', function () {
+    it('should return matching charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1']), ['ISO-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'KOI8-R', 'ISO-8859-1']), ['UTF-8', 'ISO-8859-1'])
+    })
+
+    it('should return empty list when no matching charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(['KOI8-R']), [])
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.8, ISO-8859-1', function () {
+    it('should return matching charsets in client-preferred order', function () {
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1']), ['ISO-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'KOI8-R', 'ISO-8859-1']), ['ISO-8859-1', 'UTF-8'])
+    })
+
+    it('should return empty list when no matching charsets', function () {
+      assert.deepEqual(this.negotiator.charsets(['KOI8-R']), [])
+    })
+  })
+
+  whenAcceptCharset('UTF-8;q=0.9, ISO-8859-1;q=0.8, UTF-8;q=0.7', function () {
+    it('should use highest perferred order on duplicate', function () {
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1']), ['ISO-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['UTF-8', 'ISO-8859-1']), ['UTF-8', 'ISO-8859-1'])
+      assert.deepEqual(this.negotiator.charsets(['ISO-8859-1', 'UTF-8']), ['UTF-8', 'ISO-8859-1'])
+    })
+  })
+})
+
+function createRequest(headers) {
+  var request = {
+    headers: {}
+  }
+
+  if (headers) {
+    Object.keys(headers).forEach(function (key) {
+      request.headers[key.toLowerCase()] = headers[key]
+    })
   }
 
-}).call(this);
+  return request
+}
+
+function whenAcceptCharset(acceptCharset, func) {
+  var description = !acceptCharset
+    ? 'when no Accept-Charset'
+    : 'when Accept-Charset: ' + acceptCharset
+
+  describe(description, function () {
+    before(function () {
+      this.negotiator = new Negotiator(createRequest({'Accept-Charset': acceptCharset}))
+    })
+
+    func()
+  })
+}
diff --git a/test/encoding.js b/test/encoding.js
index 7e898bc..d6cc2b5 100644
--- a/test/encoding.js
+++ b/test/encoding.js
@@ -1,101 +1,442 @@
-(function() {
-  var configuration, preferredEncodings, testConfigurations, testCorrectEncoding, _i, _len,
-    _this = this;
-
-  preferredEncodings = require('../lib/encoding').preferredEncodings;
-
-  this["Should return identity encoding when no encoding is provided"] = function(test) {
-    test.deepEqual(preferredEncodings(null), ['identity']);
-    return test.done();
-  };
-
-  this["Should include the identity encoding even if not explicity listed"] = function(test) {
-    test.ok(preferredEncodings('gzip').indexOf('identity') !== -1);
-    return test.done();
-  };
-
-  this["Should not return identity encoding if q = 0"] = function(test) {
-    test.ok(preferredEncodings('identity;q=0').indexOf('identity') === -1);
-    return test.done();
-  };
-
-  this["Should not return identity encoding if * has q = 0"] = function(test) {
-    test.ok(preferredEncodings('*;q=0').indexOf('identity') === -1);
-    return test.done();
-  };
-
-  this["Should not return identity encoding if * has q = 0 but identity explicitly has q > 0"] = function(test) {
-    test.ok(preferredEncodings('*;q=0, identity;q=0.5').indexOf('identity') !== -1);
-    return test.done();
-  };
-
-  this["Should be case insensitive"] = function(test) {
-    test.deepEqual(preferredEncodings('IDENTITY', ['identity']), ['identity']);
-    return test.done();
-  };
-
-  testCorrectEncoding = function(c) {
-    return _this["Should return " + c.selected + " for accept-encoding header " + c.accept + " with provided encoding " + c.provided] = function(test) {
-      test.deepEqual(preferredEncodings(c.accept, c.provided), c.selected);
-      return test.done();
-    };
-  };
-
-  testConfigurations = [
-    {
-      accept: undefined,
-      provided: ['identity', 'gzip'],
-      selected: ['identity']
-    }, {
-      accept: 'gzip',
-      provided: ['identity', 'gzip'],
-      selected: ['gzip', 'identity']
-    }, {
-      accept: 'gzip, compress',
-      provided: ['compress'],
-      selected: ['compress']
-    }, {
-      accept: 'deflate',
-      provided: ['gzip', 'identity'],
-      selected: ['identity']
-    }, {
-      accept: '*',
-      provided: ['identity', 'gzip'],
-      selected: ['identity', 'gzip']
-    }, {
-      accept: 'gzip, compress',
-      provided: ['compress', 'identity'],
-      selected: ['compress', 'identity']
-    }, {
-      accept: 'gzip;q=0.8, identity;q=0.5, *;q=0.3',
-      provided: ['identity', 'gzip', 'compress'],
-      selected: ['gzip', 'identity', 'compress']
-    }, {
-      accept: 'gzip;q=0.8, compress',
-      provided: ['gzip', 'compress'],
-      selected: ['compress', 'gzip']
-    }, {
-      accept: '*, compress;q=0',
-      provided: ['gzip', 'compress'],
-      selected: ['gzip']
-    }, {
-      accept: 'gzip;q=0.8, compress',
-      provided: null,
-      selected: ['compress', 'gzip', 'identity']
-    }, {
-      accept : '*, compress',
-      provided : ['gzip', 'compress'],
-      selected : ['compress', 'gzip' ]
-    }, {
-      accept : 'gzip;q=0.9, compress;q=0.8, gzip;q=0.7',
-      provided : ['gzip', 'compress'],
-      selected : ['gzip', 'compress']
-    }
-  ];
-
-  for (_i = 0, _len = testConfigurations.length; _i < _len; _i++) {
-    configuration = testConfigurations[_i];
-    testCorrectEncoding(configuration);
+
+var assert = require('assert')
+var Negotiator = require('..')
+
+describe('negotiator.encoding()', function () {
+  whenAcceptEncoding(undefined, function () {
+    it('should return identity', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'identity')
+    })
+  })
+
+  whenAcceptEncoding('*', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.encoding(), '*')
+    })
+  })
+
+  whenAcceptEncoding('*, gzip', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.encoding(), '*')
+    })
+  })
+
+  whenAcceptEncoding('*, gzip;q=0', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.encoding(), '*')
+    })
+  })
+
+  whenAcceptEncoding('*;q=0', function () {
+    it('should return undefined', function () {
+      assert.strictEqual(this.negotiator.encoding(), undefined)
+    })
+  })
+
+  whenAcceptEncoding('*;q=0, identity;q=1', function () {
+    it('should return identity', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'identity')
+    })
+  })
+
+  whenAcceptEncoding('identity', function () {
+    it('should return identity', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'identity')
+    })
+  })
+
+  whenAcceptEncoding('identity;q=0', function () {
+    it('should return undefined', function () {
+      assert.strictEqual(this.negotiator.encoding(), undefined)
+    })
+  })
+
+  whenAcceptEncoding('gzip', function () {
+    it('should return gzip', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'gzip')
+    })
+  })
+
+  whenAcceptEncoding('gzip, compress;q=0', function () {
+    it('should return gzip', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'gzip')
+    })
+  })
+
+  whenAcceptEncoding('gzip, deflate', function () {
+    it('should return gzip', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'gzip')
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, deflate', function () {
+    it('should return deflate', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'deflate')
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, identity;q=0.5, *;q=0.3', function () {
+    it('should return gzip', function () {
+      assert.strictEqual(this.negotiator.encoding(), 'gzip')
+    })
+  })
+})
+
+describe('negotiator.encoding(array)', function () {
+  whenAcceptEncoding(undefined, function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should only match identity', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+      assert.strictEqual(this.negotiator.encoding(['gzip']), undefined)
+    })
+  })
+
+  whenAcceptEncoding('*', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should return first item in list', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+      assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip')
+      assert.strictEqual(this.negotiator.encoding(['gzip', 'identity']), 'gzip')
+    })
+  })
+
+  whenAcceptEncoding('*, gzip', function () {
+    it('should prefer gzip', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+      assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip')
+      assert.strictEqual(this.negotiator.encoding(['compress', 'gzip']), 'gzip')
+    })
+  })
+
+  whenAcceptEncoding('*, gzip;q=0', function () {
+    it('should exclude gzip', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+      assert.strictEqual(this.negotiator.encoding(['gzip']), undefined)
+      assert.strictEqual(this.negotiator.encoding(['gzip', 'compress']), 'compress')
+    })
+  })
+
+  whenAcceptEncoding('*;q=0', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should match nothing', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), undefined)
+      assert.strictEqual(this.negotiator.encoding(['gzip']), undefined)
+    })
+  })
+
+  whenAcceptEncoding('*;q=0, identity;q=1', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should still match identity', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+      assert.strictEqual(this.negotiator.encoding(['gzip']), undefined)
+    })
+  })
+
+  whenAcceptEncoding('identity', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should only match identity', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+      assert.strictEqual(this.negotiator.encoding(['gzip']), undefined)
+    })
+  })
+
+  whenAcceptEncoding('identity;q=0', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should match nothing', function () {
+      assert.strictEqual(this.negotiator.encoding(['identity']), undefined)
+      assert.strictEqual(this.negotiator.encoding(['gzip']), undefined)
+    })
+  })
+
+  whenAcceptEncoding('gzip', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.encoding([]), undefined)
+    })
+
+    it('should return client-preferred encodings', function () {
+      assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip')
+      assert.strictEqual(this.negotiator.encoding(['identity', 'gzip']), 'gzip')
+      assert.strictEqual(this.negotiator.encoding(['identity']), 'identity')
+    })
+  })
+
+  whenAcceptEncoding('gzip, compress;q=0', function () {
+    it('should not return compress', function () {
+      assert.strictEqual(this.negotiator.encoding(['compress']), undefined)
+      assert.strictEqual(this.negotiator.encoding(['deflate', 'compress']), undefined)
+      assert.strictEqual(this.negotiator.encoding(['gzip', 'compress']), 'gzip')
+    })
+  })
+
+  whenAcceptEncoding('gzip, deflate', function () {
+    it('should return first client-preferred encoding', function () {
+      assert.strictEqual(this.negotiator.encoding(['deflate', 'compress']), 'deflate')
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, deflate', function () {
+    it('should return most client-preferred encoding', function () {
+      assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip')
+      assert.strictEqual(this.negotiator.encoding(['deflate']), 'deflate')
+      assert.strictEqual(this.negotiator.encoding(['deflate', 'gzip']), 'deflate')
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, identity;q=0.5, *;q=0.3', function () {
+    it('should return most client-preferred encoding', function () {
+      assert.strictEqual(this.negotiator.encoding(['gzip']), 'gzip')
+      assert.strictEqual(this.negotiator.encoding(['compress', 'identity']), 'identity')
+    })
+  })
+})
+
+describe('negotiator.encodings()', function () {
+  whenAcceptEncoding(undefined, function () {
+    it('should return identity', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['identity'])
+    })
+  })
+
+  whenAcceptEncoding('*', function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['*'])
+    })
+  })
+
+  whenAcceptEncoding('*, gzip', function () {
+    it('should prefer gzip', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['*', 'gzip'])
+    })
+  })
+
+  whenAcceptEncoding('*, gzip;q=0', function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['*'])
+    })
+  })
+
+  whenAcceptEncoding('*;q=0', function () {
+    it('should return an empty list', function () {
+      assert.deepEqual(this.negotiator.encodings(), [])
+    })
+  })
+
+  whenAcceptEncoding('*;q=0, identity;q=1', function () {
+    it('should return identity', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['identity'])
+    })
+  })
+
+  whenAcceptEncoding('identity', function () {
+    it('should return identity', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['identity'])
+    })
+  })
+
+  whenAcceptEncoding('identity;q=0', function () {
+    it('should return an empty list', function () {
+      assert.deepEqual(this.negotiator.encodings(), [])
+    })
+  })
+
+  whenAcceptEncoding('gzip', function () {
+    it('should return gzip, identity', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['gzip', 'identity'])
+    })
+  })
+
+  whenAcceptEncoding('gzip, compress;q=0', function () {
+    it('should not return compress', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['gzip', 'identity'])
+    })
+  })
+
+  whenAcceptEncoding('gzip, deflate', function () {
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['gzip', 'deflate', 'identity'])
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, deflate', function () {
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['deflate', 'gzip', 'identity'])
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, identity;q=0.5, *;q=0.3', function () {
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(), ['gzip', 'identity', '*'])
+    })
+  })
+})
+
+describe('negotiator.encodings(array)', function () {
+  whenAcceptEncoding(undefined, function () {
+    it('should return empty list for empty list', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+    })
+
+    it('should only match identity', function () {
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), [])
+    })
+  })
+
+  whenAcceptEncoding('*', function () {
+    it('should return empty list for empty list', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+    })
+
+    it('should return original list', function () {
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
+      assert.deepEqual(this.negotiator.encodings(['gzip', 'identity']), ['gzip', 'identity'])
+    })
+  })
+
+  whenAcceptEncoding('*, gzip', function () {
+    it('should prefer gzip', function () {
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
+      assert.deepEqual(this.negotiator.encodings(['compress', 'gzip']), ['gzip', 'compress'])
+    })
+  })
+
+  whenAcceptEncoding('*, gzip;q=0', function () {
+    it('should exclude gzip', function () {
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), [])
+      assert.deepEqual(this.negotiator.encodings(['gzip', 'compress']), ['compress'])
+    })
+  })
+
+  whenAcceptEncoding('*;q=0', function () {
+    it('should always return empty list', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+      assert.deepEqual(this.negotiator.encodings(['identity']), [])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), [])
+    })
+  })
+
+  whenAcceptEncoding('*;q=0, identity;q=1', function () {
+    it('should still match identity', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), [])
+    })
+  })
+
+  whenAcceptEncoding('identity', function () {
+    it('should return empty list for empty list', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+    })
+
+    it('should only match identity', function () {
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), [])
+    })
+  })
+
+  whenAcceptEncoding('identity;q=0', function () {
+    it('should always return empty list', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+      assert.deepEqual(this.negotiator.encodings(['identity']), [])
+      assert.deepEqual(this.negotiator.encodings(['gzip']), [])
+    })
+  })
+
+  whenAcceptEncoding('gzip', function () {
+    it('should return empty list for empty list', function () {
+      assert.deepEqual(this.negotiator.encodings([]), [])
+    })
+
+    it('should be case insensitive, returning provided casing', function () {
+      assert.deepEqual(this.negotiator.encodings(['GZIP']), ['GZIP'])
+      assert.deepEqual(this.negotiator.encodings(['gzip', 'GZIP']), ['gzip', 'GZIP'])
+      assert.deepEqual(this.negotiator.encodings(['GZIP', 'gzip']), ['GZIP', 'gzip'])
+    })
+
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
+      assert.deepEqual(this.negotiator.encodings(['gzip', 'identity']), ['gzip', 'identity'])
+      assert.deepEqual(this.negotiator.encodings(['identity', 'gzip']), ['gzip', 'identity'])
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+    })
+  })
+
+  whenAcceptEncoding('gzip, compress;q=0', function () {
+    it('should not return compress', function () {
+      assert.deepEqual(this.negotiator.encodings(['gzip', 'compress']), ['gzip'])
+    })
+  })
+
+  whenAcceptEncoding('gzip, deflate', function () {
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
+      assert.deepEqual(this.negotiator.encodings(['gzip', 'identity']), ['gzip', 'identity'])
+      assert.deepEqual(this.negotiator.encodings(['deflate', 'gzip']), ['gzip', 'deflate'])
+      assert.deepEqual(this.negotiator.encodings(['identity']), ['identity'])
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, deflate', function () {
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
+      assert.deepEqual(this.negotiator.encodings(['deflate']), ['deflate'])
+      assert.deepEqual(this.negotiator.encodings(['deflate', 'gzip']), ['deflate', 'gzip'])
+    })
+  })
+
+  whenAcceptEncoding('gzip;q=0.8, identity;q=0.5, *;q=0.3', function () {
+    it('should return client-preferred encodings', function () {
+      assert.deepEqual(this.negotiator.encodings(['gzip']), ['gzip'])
+      assert.deepEqual(this.negotiator.encodings(['identity', 'gzip', 'compress']), ['gzip', 'identity', 'compress'])
+    })
+  })
+})
+
+function createRequest(headers) {
+  var request = {
+    headers: {}
+  }
+
+  if (headers) {
+    Object.keys(headers).forEach(function (key) {
+      request.headers[key.toLowerCase()] = headers[key]
+    })
   }
 
-}).call(this);
+  return request
+}
+
+function whenAcceptEncoding(acceptEncoding, func) {
+  var description = !acceptEncoding
+    ? 'when no Accept-Encoding'
+    : 'when Accept-Encoding: ' + acceptEncoding
+
+  describe(description, function () {
+    before(function () {
+      this.negotiator = new Negotiator(createRequest({'Accept-Encoding': acceptEncoding}))
+    })
+
+    func()
+  })
+}
diff --git a/test/language.js b/test/language.js
index 5aad2a9..640c78b 100644
--- a/test/language.js
+++ b/test/language.js
@@ -1,110 +1,425 @@
-(function() {
-  var configuration, preferredLanguages, testConfigurations, testCorrectType, _i, _len,
-    _this = this;
-
-  preferredLanguages = require('../lib/language').preferredLanguages;
-
-  this["Should return list of languages in order"] = function(test) {
-    test.deepEqual(preferredLanguages('nl;q=0.5,fr,de,en,it,es,pt,no,se,fi'), ['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'nl']);
-    return test.done();
-  };
-
-  this["Should return list of languages in order (large list)"] = function(test) {
-    test.deepEqual(preferredLanguages('nl;q=0.5,fr,de,en,it,es,pt,no,se,fi,ro'), ['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'ro', 'nl']);
-    return test.done();
-  };
-
-  this["Should return list of languages"] = function(test) {
-    test.deepEqual(preferredLanguages('nl;q=0.5,fr,de,en,it,es,pt,no,se,fi'), ['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'nl']);
-    return test.done();
-  };
-
-  this["Should not return a language when no is provided"] = function(test) {
-    test.deepEqual(preferredLanguages('*', []), []);
-    return test.done();
-  };
-
-  this["Should not return a language when no language is acceptable"] = function(test) {
-    test.deepEqual(preferredLanguages('en', ['es']), []);
-    return test.done();
-  };
-
-  this["Should not return a language with q = 0"] = function(test) {
-    test.deepEqual(preferredLanguages('en;q=0', ['en']), []);
-    return test.done();
-  };
-
-  this["Should be case insensitive"] = function(test) {
-    test.deepEqual(preferredLanguages('en-us', ['en-US']), ['en-US']);
-    return test.done();
-  };
-
-  testCorrectType = function(c) {
-    return _this["Should return " + c.selected + " for accept-language header " + c.accept + " with provided language " + c.provided] = function(test) {
-      test.deepEqual(preferredLanguages(c.accept, c.provided), c.selected);
-      return test.done();
-    };
-  };
-
-  testConfigurations = [
-    {
-      accept: undefined,
-      provided: ['en'],
-      selected: ['en']
-    }, {
-      accept: 'en',
-      provided: ['en'],
-      selected: ['en']
-    }, {
-      accept: '*',
-      provided: ['en'],
-      selected: ['en']
-    }, {
-      accept: 'en-US, en;q=0.8',
-      provided: ['en-US', 'en-GB'],
-      selected: ['en-US', 'en-GB']
-    }, {
-      accept: 'en-US, en-GB',
-      provided: ['en-US'],
-      selected: ['en-US']
-    }, {
-      accept: 'en',
-      provided: ['en-US'],
-      selected: ['en-US']
-    }, {
-      accept: 'en;q=0.8, es',
-      provided: ['en', 'es'],
-      selected: ['es', 'en']
-    }, {
-      accept: 'en-US;q=0.8, es',
-      provided: ['en', 'es'],
-      selected: ['es', 'en']
-    }, {
-      accept: '*, en;q=0',
-      provided: ['en', 'es'],
-      selected: ['es']
-    }, {
-      accept: 'en-US;q=0.8, es',
-      provided: null,
-      selected: ['es', 'en-US']
-    }, {
-      accept: '*, en',
-      provided: ['es', 'en'],
-      selected: ['en', 'es']
-    }, {
-      accept: 'en',
-      provided: ['en', ''],
-      selected: ['en']
-    }, {
-      accept: 'en;q=0.9, es;q=0.8, en;q=0.7',
-      provided: ['en', 'es'],
-      selected: ['en', 'es']
-    }
-  ];
-
-  for (_i = 0, _len = testConfigurations.length; _i < _len; _i++) {
-    configuration = testConfigurations[_i];
-    testCorrectType(configuration);
+
+var assert = require('assert')
+var Negotiator = require('..')
+
+describe('negotiator.language()', function () {
+  whenAcceptLanguage(undefined, function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.language(), '*')
+    })
+  })
+
+  whenAcceptLanguage('*', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.language(), '*')
+    })
+  })
+
+  whenAcceptLanguage('*, en', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.language(), '*')
+    })
+  })
+
+  whenAcceptLanguage('*, en;q=0', function () {
+    it('should return *', function () {
+      assert.strictEqual(this.negotiator.language(), '*')
+    })
+  })
+
+  whenAcceptLanguage('*;q=0.8, en, es', function () {
+    it('should return en', function () {
+      assert.deepEqual(this.negotiator.language(), 'en')
+    })
+  })
+
+  whenAcceptLanguage('en', function () {
+    it('should en', function () {
+      assert.strictEqual(this.negotiator.language(), 'en')
+    })
+  })
+
+  whenAcceptLanguage('en;q=0', function () {
+    it('should return undefined', function () {
+      assert.strictEqual(this.negotiator.language(), undefined)
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.8, es', function () {
+    it('should return es', function () {
+      assert.strictEqual(this.negotiator.language(), 'es')
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.9, es;q=0.8, en;q=0.7', function () {
+    it('should return en', function () {
+      assert.strictEqual(this.negotiator.language(), 'en')
+    })
+  })
+
+  whenAcceptLanguage('en-US, en;q=0.8', function () {
+    it('should return en-US', function () {
+      assert.strictEqual(this.negotiator.language(), 'en-US')
+    })
+  })
+
+  whenAcceptLanguage('en-US, en-GB', function () {
+    it('should return en-US', function () {
+      assert.deepEqual(this.negotiator.language(), 'en-US')
+    })
+  })
+
+  whenAcceptLanguage('en-US;q=0.8, es', function () {
+    it('should return es', function () {
+      assert.strictEqual(this.negotiator.language(), 'es')
+    })
+  })
+
+  whenAcceptLanguage('nl;q=0.5, fr, de, en, it, es, pt, no, se, fi, ro', function () {
+    it('should return fr', function () {
+      assert.strictEqual(this.negotiator.language(), 'fr')
+    })
+  })
+})
+
+describe('negotiator.language(array)', function () {
+  whenAcceptLanguage(undefined, function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should return first language in list', function () {
+      assert.strictEqual(this.negotiator.language(['en']), 'en')
+      assert.strictEqual(this.negotiator.language(['es', 'en']), 'es')
+    })
+  })
+
+  whenAcceptLanguage('*', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should return first language in list', function () {
+      assert.strictEqual(this.negotiator.language(['en']), 'en')
+      assert.strictEqual(this.negotiator.language(['es', 'en']), 'es')
+    })
+  })
+
+  whenAcceptLanguage('*, en', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should return most preferred language', function () {
+      assert.strictEqual(this.negotiator.language(['en']), 'en')
+      assert.strictEqual(this.negotiator.language(['es', 'en']), 'en')
+    })
+  })
+
+  whenAcceptLanguage('*, en;q=0', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should exclude en', function () {
+      assert.strictEqual(this.negotiator.language(['en']), undefined)
+      assert.strictEqual(this.negotiator.language(['es', 'en']), 'es')
+    })
+  })
+
+  whenAcceptLanguage('*;q=0.8, en, es', function () {
+    it('should prefer en and es over everything', function () {
+      assert.deepEqual(this.negotiator.language(['en', 'nl']), 'en')
+      assert.deepEqual(this.negotiator.language(['ro', 'nl']), 'ro')
+    })
+  })
+
+  whenAcceptLanguage('en', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should return preferred langauge', function () {
+      assert.strictEqual(this.negotiator.language(['en']), 'en')
+      assert.strictEqual(this.negotiator.language(['es', 'en']), 'en')
+    })
+
+    it('should accept en-US, preferring en over en-US', function () {
+      assert.strictEqual(this.negotiator.language(['en-US']), 'en-US')
+      assert.strictEqual(this.negotiator.language(['en-US', 'en']), 'en')
+      assert.strictEqual(this.negotiator.language(['en', 'en-US']), 'en')
+    })
+  })
+
+  whenAcceptLanguage('en;q=0', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should return preferred langauge', function () {
+      assert.strictEqual(this.negotiator.language(['es', 'en']), undefined)
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.8, es', function () {
+    it('should return undefined for empty list', function () {
+      assert.strictEqual(this.negotiator.language([]), undefined)
+    })
+
+    it('should return preferred langauge', function () {
+      assert.strictEqual(this.negotiator.language(['en']), 'en')
+      assert.strictEqual(this.negotiator.language(['en', 'es']), 'es')
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.9, es;q=0.8, en;q=0.7', function () {
+    it('should use highest perferred order on duplicate', function () {
+      assert.strictEqual(this.negotiator.language(['es']), 'es')
+      assert.strictEqual(this.negotiator.language(['en', 'es']), 'en')
+      assert.strictEqual(this.negotiator.language(['es', 'en']), 'en')
+    })
+  })
+
+  whenAcceptLanguage('en-US, en;q=0.8', function () {
+    it('should use prefer en-US over en', function () {
+      assert.strictEqual(this.negotiator.language(['en', 'en-US']), 'en-US')
+      assert.strictEqual(this.negotiator.language(['en-GB', 'en-US']), 'en-US')
+      assert.strictEqual(this.negotiator.language(['en-GB', 'es']), 'en-GB')
+    })
+  })
+
+  whenAcceptLanguage('en-US, en-GB', function () {
+    it('should prefer en-US', function () {
+      assert.deepEqual(this.negotiator.language(['en-US', 'en-GB']), 'en-US')
+      assert.deepEqual(this.negotiator.language(['en-GB', 'en-US']), 'en-US')
+    })
+  })
+
+  whenAcceptLanguage('en-US;q=0.8, es', function () {
+    it('should prefer es over en-US', function () {
+      assert.strictEqual(this.negotiator.language(['es', 'en-US']), 'es')
+      assert.strictEqual(this.negotiator.language(['en-US', 'es']), 'es')
+      assert.strictEqual(this.negotiator.language(['en-US', 'en']), 'en-US')
+    })
+  })
+
+  whenAcceptLanguage('nl;q=0.5, fr, de, en, it, es, pt, no, se, fi, ro', function () {
+    it('should use prefer fr over nl', function () {
+      assert.strictEqual(this.negotiator.language(['nl', 'fr']), 'fr')
+    })
+  })
+})
+
+describe('negotiator.languages()', function () {
+  whenAcceptLanguage(undefined, function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.languages(), ['*'])
+    })
+  })
+
+  whenAcceptLanguage('*', function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.languages(), ['*'])
+    })
+  })
+
+  whenAcceptLanguage('*, en', function () {
+    it('should return *, en', function () {
+      assert.deepEqual(this.negotiator.languages(), ['*', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('*, en;q=0', function () {
+    it('should return *', function () {
+      assert.deepEqual(this.negotiator.languages(), ['*'])
+    })
+  })
+
+  whenAcceptLanguage('*;q=0.8, en, es', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(), ['en', 'es', '*'])
+    })
+  })
+
+  whenAcceptLanguage('en', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(), ['en'])
+    })
+  })
+
+  whenAcceptLanguage('en;q=0', function () {
+    it('should return empty list', function () {
+      assert.deepEqual(this.negotiator.languages(), [])
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.8, es', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(), ['es', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.9, es;q=0.8, en;q=0.7', function () {
+    it.skip('should use highest perferred order on duplicate', function () {
+      assert.deepEqual(this.negotiator.languages(), ['en', 'es'])
+    })
+  })
+
+  whenAcceptLanguage('en-US, en;q=0.8', function () {
+    it('should return en-US, en', function () {
+      assert.deepEqual(this.negotiator.languages(), ['en-US', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('en-US, en-GB', function () {
+    it('should return en-US, en-GB', function () {
+      assert.deepEqual(this.negotiator.languages(), ['en-US', 'en-GB'])
+    })
+  })
+
+  whenAcceptLanguage('en-US;q=0.8, es', function () {
+    it('should return es, en-US', function () {
+      assert.deepEqual(this.negotiator.languages(), ['es', 'en-US'])
+    })
+  })
+
+  whenAcceptLanguage('nl;q=0.5, fr, de, en, it, es, pt, no, se, fi, ro', function () {
+    it('should use prefer fr over nl', function () {
+      assert.deepEqual(this.negotiator.languages(), ['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'ro', 'nl'])
+    })
+  })
+})
+
+describe('negotiator.languages(array)', function () {
+  whenAcceptLanguage(undefined, function () {
+    it('should return original list', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['es', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('*', function () {
+    it('should return original list', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['es', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('*, en', function () {
+    it('should return list in client-preferred order', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['en', 'es'])
+    })
+  })
+
+  whenAcceptLanguage('*, en;q=0', function () {
+    it('should exclude en', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), [])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['es'])
+    })
+  })
+
+  whenAcceptLanguage('*;q=0.8, en, es', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'ro', 'nl']),
+        ['en', 'es', 'fr', 'de', 'it', 'pt', 'no', 'se', 'fi', 'ro', 'nl'])
+    })
+  })
+
+  whenAcceptLanguage('en', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['en', 'es']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['en'])
+    })
+
+    it('should accept en-US, preferring en over en-US', function () {
+      assert.deepEqual(this.negotiator.languages(['en-US']), ['en-US'])
+      assert.deepEqual(this.negotiator.languages(['en-US', 'en']), ['en', 'en-US'])
+      assert.deepEqual(this.negotiator.languages(['en', 'en-US']), ['en', 'en-US'])
+    })
+  })
+
+  whenAcceptLanguage('en;q=0', function () {
+    it('should return nothing', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), [])
+      assert.deepEqual(this.negotiator.languages(['en', 'es']), [])
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.8, es', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['en', 'es']), ['es', 'en'])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['es', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('en;q=0.9, es;q=0.8, en;q=0.7', function () {
+    it.skip('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(['en']), ['en'])
+      assert.deepEqual(this.negotiator.languages(['en', 'es']), ['es', 'en'])
+      assert.deepEqual(this.negotiator.languages(['es', 'en']), ['es', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('en-US, en;q=0.8', function () {
+    it('should be case insensitive', function () {
+      assert.deepEqual(this.negotiator.languages(['en-us', 'EN']), ['en-us', 'EN'])
+    })
+
+    it('should prefer en-US over en', function () {
+      assert.deepEqual(this.negotiator.languages(['en-US', 'en']), ['en-US', 'en'])
+      assert.deepEqual(this.negotiator.languages(['en-GB', 'en-US', 'en']), ['en-US', 'en', 'en-GB'])
+    })
+  })
+
+  whenAcceptLanguage('en-US, en-GB', function () {
+    it('should prefer en-US over en-GB', function () {
+      assert.deepEqual(this.negotiator.languages(['en-US', 'en-GB']), ['en-US', 'en-GB'])
+      assert.deepEqual(this.negotiator.languages(['en-GB', 'en-US']), ['en-US', 'en-GB'])
+    })
+  })
+
+  whenAcceptLanguage('en-US;q=0.8, es', function () {
+    it('should prefer es over en-US', function () {
+      assert.deepEqual(this.negotiator.languages(['en', 'es']), ['es', 'en'])
+      assert.deepEqual(this.negotiator.languages(['en', 'es', 'en-US']), ['es', 'en-US', 'en'])
+    })
+  })
+
+  whenAcceptLanguage('nl;q=0.5, fr, de, en, it, es, pt, no, se, fi, ro', function () {
+    it('should return preferred languages', function () {
+      assert.deepEqual(this.negotiator.languages(['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'ro', 'nl']),
+       ['fr', 'de', 'en', 'it', 'es', 'pt', 'no', 'se', 'fi', 'ro', 'nl'])
+    })
+  })
+})
+
+function createRequest(headers) {
+  var request = {
+    headers: {}
+  }
+
+  if (headers) {
+    Object.keys(headers).forEach(function (key) {
+      request.headers[key.toLowerCase()] = headers[key]
+    })
   }
 
-}).call(this);
+  return request
+}
+
+function whenAcceptLanguage(acceptLanguage, func) {
+  var description = !acceptLanguage
+    ? 'when no Accept-Language'
+    : 'when Accept-Language: ' + acceptLanguage
+
+  describe(description, function () {
+    before(function () {
+      this.negotiator = new Negotiator(createRequest({'Accept-Language': acceptLanguage}))
+    })
+
+    func()
+  })
+}
diff --git a/test/mediaType.js b/test/mediaType.js
index 6cf0795..8365be7 100644
--- a/test/mediaType.js
+++ b/test/mediaType.js
@@ -1,140 +1,499 @@
-(function() {
-  var configuration, preferredMediaTypes, testConfigurations, testCorrectType, _i, _len,
-    _this = this;
-
-  preferredMediaTypes = require('../lib/mediaType').preferredMediaTypes;
-
-  this["Should not return a media type when no media type provided"] = function(test) {
-    test.deepEqual(preferredMediaTypes('*/*', []), []);
-    return test.done();
-  };
-
-  this["Should not return a media type when no media type is acceptable"] = function(test) {
-    test.deepEqual(preferredMediaTypes('application/json', ['text/html']), []);
-    return test.done();
-  };
-
-  this["Should not return a media type with q = 0"] = function(test) {
-    test.deepEqual(preferredMediaTypes('text/html;q=0', ['text/html']), []);
-    return test.done();
-  };
-
-  this["Should handle extra slashes on query params"] = function(test) {
-    var type = 'application/xhtml+xml;profile="http://www.wapforum.org/xhtml"'
-    test.deepEqual(preferredMediaTypes(type, ['application/xhtml+xml;profile="http://www.wapforum.org/xhtml"']), ['application/xhtml+xml;profile="http://www.wapforum.org/xhtml"']);
-
-    return test.done();
-  };
-
-  this["Should be case insensitive"] = function(test) {
-    test.deepEqual(preferredMediaTypes('application/JSON', ['application/json']), ['application/json']);
-    return test.done();
-  };
-
-
-  testCorrectType = function(c) {
-    return _this["Should return " + c.selected + " for access header " + c.accept + " with provided types " + c.provided] = function(test) {
-      test.deepEqual(preferredMediaTypes(c.accept, c.provided), c.selected);
-      return test.done();
-    };
-  };
-
-  testConfigurations = [
-    {
-      accept: undefined,
-      provided: ['text/html'],
-      selected: ['text/html']
-    }, {
-      accept: 'text/html',
-      provided: ['text/html'],
-      selected: ['text/html']
-    }, {
-      accept: '*/*',
-      provided: ['text/html'],
-      selected: ['text/html']
-    }, {
-      accept: 'text/*',
-      provided: ['text/html'],
-      selected: ['text/html']
-    }, {
-      accept: 'application/json, text/html',
-      provided: ['text/html'],
-      selected: ['text/html']
-    }, {
-      accept: 'text/html;q=0.1',
-      provided: ['text/html'],
-      selected: ['text/html']
-    }, {
-      accept: 'application/json, text/html',
-      provided: ['application/json', 'text/html'],
-      selected: ['application/json', 'text/html']
-    }, {
-      accept: 'application/json;q=0.2, text/html',
-      provided: ['application/json', 'text/html'],
-      selected: ['text/html', 'application/json']
-    }, {
-      accept: 'application/json;q=0.2, text/html',
-      provided: null,
-      selected: ['text/html', 'application/json']
-    }, {
-      accept: 'text/*, text/html;q=0',
-      provided: ['text/html', 'text/plain'],
-      selected: ['text/plain']
-    }, {
-      accept: 'text/*, text/html;q=0.5',
-      provided: ['text/html', 'text/plain'],
-      selected: ['text/plain', 'text/html']
-    }, {
-      accept: 'application/json, */*; q=0.01',
-      provided: ['text/html', 'application/json'],
-      selected: ['application/json', 'text/html']
-    }, {
-      accept: 'application/vnd.example;attribute=value',
-      provided: ['application/vnd.example;attribute=other', 'application/vnd.example;attribute=value'],
-      selected: ['application/vnd.example;attribute=value']
-    }, {
-      accept: 'application/vnd.example;attribute=other',
-      provided: ['application/vnd.example', 'application/vnd.example;attribute=other'],
-      selected: ['application/vnd.example;attribute=other']
-    }, {
-      accept: 'text/html;level=1',
-      provided: ['text/html;level=1;foo=bar'],
-      selected: ['text/html;level=1;foo=bar']
-    }, {
-      accept: 'text/html;level=1;foo=bar',
-      provided: ['text/html;level=1'],
-      selected: []
-    }, {
-      accept: 'text/html;level=2',
-      provided: ['text/html;level=1'],
-      selected: []
-    }, {
-      accept : 'text/html, text/html;level=1;q=0.1',
-      provided : ['text/html', 'text/html;level=1'],
-      selected : ['text/html', 'text/html;level=1']
-    }, {
-      accept : 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5',
-      provided : ['text/html;level=1', 'text/html', 'text/html;level=3', 'image/jpeg', 'text/html;level=2', 'text/plain'],
-      selected : ['text/html;level=1', 'text/html', 'text/html;level=3', 'image/jpeg', 'text/html;level=2', 'text/plain']
-    }, {
-      accept : 'text/html, application/xhtml+xml, */*',
-      provided : ['application/json', 'text/html'],
-      selected : ['text/html', 'application/json' ]
-    }, {
-      accept : 'text/html, application/json',
-      provided : ['text/html', 'boom'],
-      selected : ['text/html']
-    }, {
-      accept: 'application/json;q=0.9, text/html;q=0.8, application/json;q=0.7',
-      provided: ['application/json', 'text/html'],
-      selected: ['application/json', 'text/html']
-    }
-
-  ];
-
-  for (_i = 0, _len = testConfigurations.length; _i < _len; _i++) {
-    configuration = testConfigurations[_i];
-    testCorrectType(configuration);
+
+var assert = require('assert')
+var Negotiator = require('..')
+
+describe('negotiator.mediaType()', function () {
+  whenAccept(undefined, function () {
+    it('should return */*', function () {
+      assert.strictEqual(this.negotiator.mediaType(), '*/*')
+    })
+  })
+
+  whenAccept('*/*', function () {
+    it('should return */*', function () {
+      assert.strictEqual(this.negotiator.mediaType(), '*/*')
+    })
+  })
+
+  whenAccept('application/json', function () {
+    it('should return application/json', function () {
+      assert.deepEqual(this.negotiator.mediaType(), 'application/json')
+    })
+  })
+
+  whenAccept('application/json;q=0', function () {
+    it('should return undefined', function () {
+      assert.strictEqual(this.negotiator.mediaType(), undefined)
+    })
+  })
+
+  whenAccept('application/json;q=0.2, text/html', function () {
+    it('should return text/html', function () {
+      assert.deepEqual(this.negotiator.mediaType(), 'text/html')
+    })
+  })
+
+  whenAccept('text/*', function () {
+    it('should return text/*', function () {
+      assert.strictEqual(this.negotiator.mediaType(), 'text/*')
+    })
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, */*;q=0.1', function () {
+    it('should return text/plain', function () {
+      assert.strictEqual(this.negotiator.mediaType(), 'text/plain')
+    })
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, text/xml, text/yaml, text/javascript, text/csv, text/css, text/rtf, text/markdown, application/octet-stream;q=0.2, */*;q=0.1', function () {
+    it('should return text/plain', function () {
+      assert.strictEqual(this.negotiator.mediaType(), 'text/plain')
+    })
+  })
+})
+
+describe('negotiator.mediaType(array)', function () {
+  whenAccept(undefined, function () {
+    it('should return first item in list', function () {
+      assert.strictEqual(this.negotiator.mediaType(['text/html']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['text/html', 'application/json']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['application/json', 'text/html']), 'application/json')
+    })
+  })
+
+  whenAccept('*/*', function () {
+    it('should return first item in list', function () {
+      assert.strictEqual(this.negotiator.mediaType(['text/html']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['text/html', 'application/json']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['application/json', 'text/html']), 'application/json')
+    })
+  })
+
+  whenAccept('application/json', function () {
+    it('should be case insensitive', function () {
+      assert.strictEqual(this.negotiator.mediaType(['application/JSON']), 'application/JSON')
+    })
+
+    it('should only return application/json', function () {
+      assert.strictEqual(this.negotiator.mediaType(['text/html']), undefined)
+      assert.strictEqual(this.negotiator.mediaType(['text/html', 'application/json']), 'application/json')
+    })
+  })
+
+  whenAccept('application/json;q=0', function () {
+    it('should return undefined', function () {
+      assert.strictEqual(this.negotiator.mediaType(), undefined)
+    })
+  })
+
+  whenAccept('application/json;q=0.2, text/html', function () {
+    it('should prefer text/html over application/json', function () {
+      assert.strictEqual(this.negotiator.mediaType(['application/json']), 'application/json')
+      assert.strictEqual(this.negotiator.mediaType(['application/json', 'text/html']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['text/html', 'application/json']), 'text/html')
+    })
+  })
+
+  whenAccept('text/*', function () {
+    it('should prefer text media types', function () {
+      assert.strictEqual(this.negotiator.mediaType(['application/json']), undefined)
+      assert.strictEqual(this.negotiator.mediaType(['application/json', 'text/html']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['text/html', 'application/json']), 'text/html')
+    })
+  })
+
+  whenAccept('text/*, text/plain;q=0', function () {
+    it('should prefer text media types', function () {
+      assert.strictEqual(this.negotiator.mediaType(['application/json']), undefined)
+      assert.strictEqual(this.negotiator.mediaType(['application/json', 'text/html']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['text/html', 'application/json']), 'text/html')
+    })
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, */*;q=0.1', function () {
+    it('should return in preferred order', function () {
+      assert.strictEqual(this.negotiator.mediaType(['application/json', 'text/plain', 'text/html']), 'text/plain')
+      assert.strictEqual(this.negotiator.mediaType(['image/jpeg', 'text/html']), 'text/html')
+      assert.strictEqual(this.negotiator.mediaType(['image/jpeg', 'image/gif']), 'image/jpeg')
+    })
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, text/xml, text/yaml, text/javascript, text/csv, text/css, text/rtf, text/markdown, application/octet-stream;q=0.2, */*;q=0.1', function () {
+    it('should return the client-preferred order', function () {
+      assert.strictEqual(this.negotiator.mediaType(['text/plain', 'text/html', 'text/xml', 'text/yaml', 'text/javascript', 'text/csv', 'text/css', 'text/rtf', 'text/markdown', 'application/json', 'application/octet-stream']),
+        'text/plain')
+    })
+  })
+})
+
+describe('negotiator.mediaTypes()', function () {
+  whenAccept(undefined, function () {
+    it('should return */*', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['*/*'])
+    })
+  })
+
+  whenAccept('*/*', function () {
+    it('should return */*', mediaTypesPreferred(
+      ['*/*']
+    ))
+  })
+
+  whenAccept('application/json', function () {
+    it('should return application/json', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['application/json'])
+    })
+  })
+
+  whenAccept('application/json;q=0', function () {
+    it('should return empty list', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), [])
+    })
+  })
+
+  whenAccept('application/json;q=0.2, text/html', function () {
+    it('should return text/html, application/json', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/html', 'application/json'])
+    })
+  })
+
+  whenAccept('text/*', function () {
+    it('should return text/*', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/*'])
+    })
+  })
+
+  whenAccept('text/*, text/plain;q=0', function () {
+    it('should return text/*', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/*'])
+    })
+  })
+
+  whenAccept('text/html;LEVEL=1', function () {
+    it('should return text/html;LEVEL=1', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/html'])
+    })
+  })
+
+  whenAccept('text/html;foo="bar,text/css;";fizz="buzz,5", text/plain', function () {
+    it('should return text/html, text/plain', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/html', 'text/plain'])
+    })
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, */*;q=0.1', function () {
+    it('should return text/plain, text/html, application/json, */*', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/plain', 'text/html', 'application/json', '*/*'])
+    })
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, text/xml, text/yaml, text/javascript, text/csv, text/css, text/rtf, text/markdown, application/octet-stream;q=0.2, */*;q=0.1', function () {
+    it('should return the client-preferred order', function () {
+      assert.deepEqual(this.negotiator.mediaTypes(), ['text/plain', 'text/html', 'text/xml', 'text/yaml', 'text/javascript', 'text/csv', 'text/css', 'text/rtf', 'text/markdown', 'application/json', 'application/octet-stream', '*/*'])
+    })
+  })
+})
+
+describe('negotiator.mediaTypes(array)', function () {
+  whenAccept(undefined, function () {
+    it('should return return original list', mediaTypesNegotiated(
+      ['application/json', 'text/plain'],
+      ['application/json', 'text/plain']
+    ))
+  })
+
+  whenAccept('*/*', function () {
+    it('should return return original list', mediaTypesNegotiated(
+      ['application/json', 'text/plain'],
+      ['application/json', 'text/plain']
+    ))
+  })
+
+  whenAccept('*/*;q=0.8, text/*, image/*', function () {
+    it('should return return stable-sorted list', mediaTypesNegotiated(
+      ['application/json', 'text/html', 'text/plain', 'text/xml', 'application/xml', 'image/gif', 'image/jpeg', 'image/png', 'audio/mp3', 'application/javascript', 'text/javascript'],
+      ['text/html', 'text/plain', 'text/xml', 'text/javascript', 'image/gif', 'image/jpeg', 'image/png', 'application/json', 'application/xml', 'audio/mp3', 'application/javascript']
+    ))
+  })
+
+  whenAccept('application/json', function () {
+    it('should accept application/json', mediaTypesNegotiated(
+      ['application/json'],
+      ['application/json']
+    ))
+
+    it('should be case insensitive', mediaTypesNegotiated(
+      ['application/JSON'],
+      ['application/JSON']
+    ))
+
+    it('should only return application/json', mediaTypesNegotiated(
+      ['text/html', 'application/json'],
+      ['application/json']
+    ))
+
+    it('should ignore invalid types', mediaTypesNegotiated(
+      ['boom', 'application/json'],
+      ['application/json']
+    ))
+  })
+
+  whenAccept('application/json;q=0', function () {
+    it('should not accept application/json', mediaTypesNegotiated(
+      ['application/json'],
+      []
+    ))
+
+    it('should not accept other media types', mediaTypesNegotiated(
+      ['application/json', 'text/html', 'image/jpeg'],
+      []
+    ))
+  })
+
+  whenAccept('application/json;q=0.2, text/html', function () {
+    it('should prefer text/html over application/json', mediaTypesNegotiated(
+      ['application/json', 'text/html'],
+      ['text/html', 'application/json']
+    ))
+  })
+
+  whenAccept('application/json;q=0.9, text/html;q=0.8, application/json;q=0.7', function () {
+    it('should prefer application/json over text/html', mediaTypesNegotiated(
+      ['text/html', 'application/json'],
+      ['application/json', 'text/html']
+    ))
+  })
+
+  whenAccept('application/json, */*;q=0.1', function () {
+    it('should prefer application/json over text/html', mediaTypesNegotiated(
+      ['text/html', 'application/json'],
+      ['application/json', 'text/html']
+    ))
+  })
+
+  whenAccept('application/xhtml+xml;profile="http://www.wapforum.org/xhtml"', function () {
+    it('should accept application/xhtml+xml;profile="http://www.wapforum.org/xhtml"', mediaTypesNegotiated(
+      ['application/xhtml+xml;profile="http://www.wapforum.org/xhtml"'],
+      ['application/xhtml+xml;profile="http://www.wapforum.org/xhtml"']
+    ))
+  })
+
+  whenAccept('text/*', function () {
+    it('should prefer text media types', mediaTypesNegotiated(
+      ['text/html', 'application/json', 'text/plain'],
+      ['text/html', 'text/plain']
+    ))
+  })
+
+  whenAccept('text/*, text/html;level', function () {
+    it('should accept text/html', mediaTypesNegotiated(
+      ['text/html'],
+      ['text/html']
+    ))
+  })
+
+  whenAccept('text/*, text/plain;q=0', function () {
+    it('should prefer text media types except text/plain', mediaTypesNegotiated(
+      ['text/html', 'text/plain'],
+      ['text/html']
+    ))
+  })
+
+  whenAccept('text/*, text/plain;q=0.5', function () {
+    it('should prefer text/plain below other text types', mediaTypesNegotiated(
+      ['text/html', 'text/plain', 'text/xml'],
+      ['text/html', 'text/xml', 'text/plain']
+    ))
+  })
+
+  whenAccept('text/html;level=1', function () {
+    it('should accept text/html;level=1', mediaTypesNegotiated(
+      ['text/html;level=1'],
+      ['text/html;level=1']
+    ))
+
+    it('should accept text/html;Level=1', mediaTypesNegotiated(
+      ['text/html;Level=1'],
+      ['text/html;Level=1']
+    ))
+
+    it('should not accept text/html;level=2', mediaTypesNegotiated(
+      ['text/html;level=2'],
+      []
+    ))
+
+    it('should not accept text/html', mediaTypesNegotiated(
+      ['text/html'],
+      []
+    ))
+
+    it('should accept text/html;level=1;foo=bar', mediaTypesNegotiated(
+      ['text/html;level=1;foo=bar'],
+      ['text/html;level=1;foo=bar']
+    ))
+  })
+
+  whenAccept('text/html;level=1;foo=bar', function () {
+    it('should not accept text/html;level=1', mediaTypesNegotiated(
+      ['text/html;level=1'],
+      []
+    ))
+
+    it('should accept text/html;level=1;foo=bar', mediaTypesNegotiated(
+      ['text/html;level=1;foo=bar'],
+      ['text/html;level=1;foo=bar']
+    ))
+
+    it('should accept text/html;foo=bar;level=1', mediaTypesNegotiated(
+      ['text/html;foo=bar;level=1'],
+      ['text/html;foo=bar;level=1']
+    ))
+  })
+
+  whenAccept('text/html;level=1;foo="bar"', function () {
+    it('should accept text/html;level=1;foo=bar', mediaTypesNegotiated(
+      ['text/html;level=1;foo=bar'],
+      ['text/html;level=1;foo=bar']
+    ))
+
+    it('should accept text/html;level=1;foo="bar"', mediaTypesNegotiated(
+      ['text/html;level=1;foo="bar"'],
+      ['text/html;level=1;foo="bar"']
+    ))
+  })
+
+  whenAccept('text/html;foo=";level=2;"', function () {
+    it('should not accept text/html;level=2', mediaTypesNegotiated(
+      ['text/html;level=2'],
+      []
+    ))
+
+    it('should accept text/html;foo=";level=2;"', mediaTypesNegotiated(
+      ['text/html;foo=";level=2;"'],
+      ['text/html;foo=";level=2;"']
+    ))
+  })
+
+  whenAccept('text/html;LEVEL=1', function () {
+    it('should accept text/html;level=1', mediaTypesNegotiated(
+      ['text/html;level=1'],
+      ['text/html;level=1']
+    ))
+
+    it('should accept text/html;Level=1', mediaTypesNegotiated(
+      ['text/html;Level=1'],
+      ['text/html;Level=1']
+    ))
+  })
+
+  whenAccept('text/html;LEVEL=1;level=2', function () {
+    it('should accept text/html;level=2', mediaTypesNegotiated(
+      ['text/html;level=2'],
+      ['text/html;level=2']
+    ))
+
+    it('should not accept text/html;level=1', mediaTypesNegotiated(
+      ['text/html;level=1'],
+      []
+    ))
+  })
+
+  whenAccept('text/html;level=2', function () {
+    it('should not accept text/html;level=1', mediaTypesNegotiated(
+      ['text/html;level=1'],
+      []
+    ))
+  })
+
+  whenAccept('text/html;level=2, text/html', function () {
+    it('should prefer text/html;level=2 over text/html', mediaTypesNegotiated(
+      ['text/html', 'text/html;level=2'],
+      ['text/html;level=2', 'text/html']
+    ))
+  })
+
+  whenAccept('text/html;level=2;q=0.1, text/html', function () {
+    it('should prefer text/html over text/html;level=2', mediaTypesNegotiated(
+      ['text/html;level=2', 'text/html'],
+      ['text/html', 'text/html;level=2']
+    ))
+  })
+
+  whenAccept('text/html;level=2;q=0.1;level=1', function () {
+    it('should not accept text/html;level=1', mediaTypesNegotiated(
+      ['text/html;level=1'],
+      []
+    ))
+  })
+
+  whenAccept('text/html;level=2;q=0.1, text/html;level=1, text/html;q=0.5', function () {
+    it('should prefer text/html;level=1, text/html, text/html;level=2', mediaTypesNegotiated(
+      ['text/html;level=1', 'text/html;level=2', 'text/html'],
+      ['text/html;level=1', 'text/html', 'text/html;level=2']
+    ))
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, */*;q=0.1', function () {
+    it('should prefer text/plain over text/html', mediaTypesNegotiated(
+      ['text/html', 'text/plain'],
+      ['text/plain', 'text/html']
+    ))
+
+    it('should prefer application/json after text', mediaTypesNegotiated(
+      ['application/json', 'text/html', 'text/plain'],
+      ['text/plain', 'text/html', 'application/json']
+    ))
+
+    it('should prefer image/jpeg after text', mediaTypesNegotiated(
+      ['image/jpeg', 'text/html', 'text/plain'],
+      ['text/plain', 'text/html', 'image/jpeg']
+    ))
+  })
+
+  whenAccept('text/plain, application/json;q=0.5, text/html, text/xml, text/yaml, text/javascript, text/csv, text/css, text/rtf, text/markdown, application/octet-stream;q=0.2, */*;q=0.1', function () {
+    it('should return the client-preferred order', mediaTypesNegotiated(
+      ['text/plain', 'text/html', 'text/xml', 'text/yaml', 'text/javascript', 'text/csv', 'text/css', 'text/rtf', 'text/markdown', 'application/json', 'application/octet-stream'],
+      ['text/plain', 'text/html', 'text/xml', 'text/yaml', 'text/javascript', 'text/csv', 'text/css', 'text/rtf', 'text/markdown', 'application/json', 'application/octet-stream']
+    ))
+  })
+})
+
+function createRequest(headers) {
+  var request = {
+    headers: {}
+  }
+
+  if (headers) {
+    Object.keys(headers).forEach(function (key) {
+      request.headers[key.toLowerCase()] = headers[key]
+    })
+  }
+
+  return request
+}
+
+function mediaTypesNegotiated(serverTypes, preferredTypes) {
+  return function () {
+    assert.deepEqual(this.negotiator.mediaTypes(serverTypes), preferredTypes)
+  }
+}
+
+function mediaTypesPreferred(preferredTypes) {
+  return function () {
+    assert.deepEqual(this.negotiator.mediaTypes(), preferredTypes)
   }
+}
+
+function whenAccept(accept, func) {
+  var description = !accept
+    ? 'when no Accept'
+    : 'when Accept: ' + accept
+
+  describe(description, function () {
+    before(function () {
+      this.negotiator = Negotiator(createRequest({'Accept': accept}))
+    })
 
-}).call(this);
+    func()
+  })
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-negotiator.git



More information about the Pkg-javascript-commits mailing list