Browse Source

Add URL and QueryString modules, and tests for each.

Also, make a slight change from original on url-module to put the
spacePattern into the function.  On closer inspection, it turns out that the
nonlocal-var cost is higher than the compiling-a-regexp cost.

Also, documentation.
v0.7.4-release
isaacs 15 years ago
committed by Ryan Dahl
parent
commit
7ff04c1f86
  1. 93
      doc/api.txt
  2. 177
      lib/querystring.js
  3. 299
      lib/url.js
  4. 125
      test/mjsunit/test-querystring.js
  5. 495
      test/mjsunit/test-url.js

93
doc/api.txt

@ -1531,6 +1531,99 @@ require("path").exists("/etc/passwd", function (exists) {
------------------------------------
=== URL Module
This module has utilities for URL resolution and parsing.
Parsed URL objects have some or all of the following fields, depending on whether or not
they exist in the URL string. Any parts that are not in the URL string will not be in the
parsed object. Examples are shown for the URL +"http://user:pass@host.com:8080/p/a/t/h?query=string#hash"+
+href+::
The full URL that was originally parsed. Example: +"http://user:pass@host.com:8080/p/a/t/h?query=string#hash"+
+protocol+::
The request protocol. Example: +"http:"+
+host+::
The full host portion of the URL, including port and authentication information. Example:
+"user:pass@host.com:8080"+
+auth+::
The authentication information portion of a URL. Example: +"user:pass"+
+hostname+::
Just the hostname portion of the host. Example: +"host.com"+
+port+::
The port number portion of the host. Example: +"8080"+
+pathname+::
The path section of the URL, that comes after the host and before the query, including the
initial slash if present. Example: +"/p/a/t/h"+
+search+::
The "query string" portion of the URL, including the leading question mark. Example:
+"?query=string"+
+query+::
Either the "params" portion of the query string, or a querystring-parsed object. Example:
+"query=string"+ or +{"query":"string"}+
+hash+::
The portion of the URL after the pound-sign. Example: +"#hash"+
The following methods are provided by the URL module:
+url.parse(urlStr, parseQueryString=false)+::
Take a URL string, and return an object. Pass +true+ as the second argument to also parse
the query string using the +querystring+ module.
+url.format(urlObj)+::
Take a parsed URL object, and return a formatted URL string.
+url.resolve(from, to)+::
Take a base URL, and a href URL, and resolve them as a browser would for an anchor tag.
=== Query String Module
This module provides utilities for dealing with query strings. It provides the following methods:
+querystring.stringify(obj, sep="&", eq="=")+::
Serialize an object to a query string. Optionally override the default separator and assignment characters.
Example:
+
------------------------------------
node> require("querystring").stringify({foo:"bar", baz : {quux:"asdf", oof : "rab"}, boo:[1,2,3]})
"foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1&boo%5B%5D=2&boo%5B%5D=3"
------------------------------------
+
+querystring.parse(str, sep="&", eq="=")+::
Deserialize a query string to an object. Optionally override the default separator and assignment characters.
+
------------------------------------
node> require("querystring").parse("foo=bar&baz%5Bquux%5D=asdf&baz%5Boof%5D=rab&boo%5B%5D=1")
{
"foo": "bar",
"baz": {
"quux": "asdf",
"oof": "rab"
},
"boo": [
1
]
}
------------------------------------
+
+querystring.escape+
The escape function used by +querystring.stringify+, provided so that it could be overridden if necessary.
+querystring.unescape+
The unescape function used by +querystring.parse+, provided so that it could be overridden if necessary.
== REPL
A Read-Eval-Print-Loop is available both as a standalone program and easily

177
lib/querystring.js

@ -0,0 +1,177 @@
// Query String Utilities
var QueryString = exports;
QueryString.unescape = function (str, decodeSpaces) {
return decodeURIComponent(decodeSpaces ? str.replace(/\+/g, " ") : str);
};
QueryString.escape = function (str) {
return encodeURIComponent(str);
};
var stack = [];
/**
* <p>Converts an arbitrary value to a Query String representation.</p>
*
* <p>Objects with cyclical references will trigger an exception.</p>
*
* @method stringify
* @param obj {Variant} any arbitrary value to convert to query string
* @param sep {String} (optional) Character that should join param k=v pairs together. Default: "&"
* @param eq {String} (optional) Character that should join keys to their values. Default: "="
* @param name {String} (optional) Name of the current key, for handling children recursively.
* @static
*/
QueryString.stringify = function (obj, sep, eq, name) {
sep = sep || "&";
eq = eq || "=";
if (isA(obj, null) || isA(obj, undefined) || typeof(obj) === 'function') {
return name ? encodeURIComponent(name) + eq : '';
}
if (isBool(obj)) obj = +obj;
if (isNumber(obj) || isString(obj)) {
return encodeURIComponent(name) + eq + encodeURIComponent(obj);
}
if (isA(obj, [])) {
var s = [];
name = name+'[]';
for (var i = 0, l = obj.length; i < l; i ++) {
s.push( QueryString.stringify(obj[i], sep, eq, name) );
}
return s.join(sep);
}
// now we know it's an object.
// Check for cyclical references in nested objects
for (var i = stack.length - 1; i >= 0; --i) if (stack[i] === obj) {
throw new Error("querystring.stringify. Cyclical reference");
}
stack.push(obj);
var s = [];
var begin = name ? name + '[' : '';
var end = name ? ']' : '';
for (var i in obj) if (obj.hasOwnProperty(i)) {
var n = begin + i + end;
s.push(QueryString.stringify(obj[i], sep, eq, n));
}
stack.pop();
s = s.join(sep);
if (!s && name) return name + "=";
return s;
};
QueryString.parseQuery = QueryString.parse = function (qs, sep, eq) {
return qs
.split(sep||"&")
.map(pieceParser(eq||"="))
.reduce(mergeParams);
};
// Parse a key=val string.
// These can get pretty hairy
// example flow:
// parse(foo[bar][][bla]=baz)
// return parse(foo[bar][][bla],"baz")
// return parse(foo[bar][], {bla : "baz"})
// return parse(foo[bar], [{bla:"baz"}])
// return parse(foo, {bar:[{bla:"baz"}]})
// return {foo:{bar:[{bla:"baz"}]}}
var trimmerPattern = /^\s+|\s+$/g,
slicerPattern = /(.*)\[([^\]]*)\]$/;
var pieceParser = function (eq) {
return function parsePiece (key, val) {
if (arguments.length !== 2) {
// key=val, called from the map/reduce
key = key.split(eq);
return parsePiece(
QueryString.unescape(key.shift(), true),
QueryString.unescape(key.join(eq), true)
);
}
key = key.replace(trimmerPattern, '');
if (isString(val)) {
val = val.replace(trimmerPattern, '');
// convert numerals to numbers
if (!isNaN(val)) {
var numVal = +val;
if (val === numVal.toString(10)) val = numVal;
}
}
var sliced = slicerPattern.exec(key);
if (!sliced) {
var ret = {};
if (key) ret[key] = val;
return ret;
}
// ["foo[][bar][][baz]", "foo[][bar][]", "baz"]
var tail = sliced[2], head = sliced[1];
// array: key[]=val
if (!tail) return parsePiece(head, [val]);
// obj: key[subkey]=val
var ret = {};
ret[tail] = val;
return parsePiece(head, ret);
};
};
// the reducer function that merges each query piece together into one set of params
function mergeParams (params, addition) {
return (
// if it's uncontested, then just return the addition.
(!params) ? addition
// if the existing value is an array, then concat it.
: (isA(params, [])) ? params.concat(addition)
// if the existing value is not an array, and either are not objects, arrayify it.
: (!isA(params, {}) || !isA(addition, {})) ? [params].concat(addition)
// else merge them as objects, which is a little more complex
: mergeObjects(params, addition)
);
};
// Merge two *objects* together. If this is called, we've already ruled
// out the simple cases, and need to do the for-in business.
function mergeObjects (params, addition) {
for (var i in addition) if (i && addition.hasOwnProperty(i)) {
params[i] = mergeParams(params[i], addition[i]);
}
return params;
};
// duck typing
function isA (thing, canon) {
return (
// truthiness. you can feel it in your gut.
(!thing === !canon)
// typeof is usually "object"
&& typeof(thing) === typeof(canon)
// check the constructor
&& Object.prototype.toString.call(thing) === Object.prototype.toString.call(canon)
);
};
function isBool (thing) {
return (
typeof(thing) === "boolean"
|| isA(thing, new Boolean(thing))
);
};
function isNumber (thing) {
return (
typeof(thing) === "number"
|| isA(thing, new Number(thing))
) && isFinite(thing);
};
function isString (thing) {
return (
typeof(thing) === "string"
|| isA(thing, new String(thing))
);
};

299
lib/url.js

@ -0,0 +1,299 @@
exports.parse = url_parse;
exports.resolve = url_resolve;
exports.resolveObject = url_resolveObject;
exports.format = url_format;
// define these here so at least they only have to be compiled once on the first module load.
var protocolPattern = /^([a-z0-9]+:)/,
portPattern = /:[0-9]+$/,
nonHostChars = ["/", "?", ";", "#"],
hostlessProtocol = {
"file":true,
"file:":true
},
slashedProtocol = {
"http":true, "https":true, "ftp":true, "gopher":true, "file":true,
"http:":true, "https:":true, "ftp:":true, "gopher:":true, "file:":true
},
path = require("path"), // internal module, guaranteed to be loaded already.
querystring; // don't load unless necessary.
function url_parse (url, parseQueryString) {
if (url && typeof(url) === "object" && url.href) return url;
var out = { href : url },
rest = url;
var proto = protocolPattern.exec(rest);
if (proto) {
proto = proto[0];
out.protocol = proto;
rest = rest.substr(proto.length);
}
// figure out if it's got a host
var slashes = rest.substr(0, 2) === "//";
if (slashes && !(proto && hostlessProtocol[proto])) {
rest = rest.substr(2);
out.slashes = true;
}
if (!hostlessProtocol[proto] && (slashes || (proto && !slashedProtocol[proto]))) {
// there's a hostname.
// the first instance of /, ?, ;, or # ends the host.
// don't enforce full RFC correctness, just be unstupid about it.
var firstNonHost = -1;
for (var i = 0, l = nonHostChars.length; i < l; i ++) {
var index = rest.indexOf(nonHostChars[i]);
if (index !== -1 && (firstNonHost < 0 || index < firstNonHost)) firstNonHost = index;
}
if (firstNonHost !== -1) {
out.host = rest.substr(0, firstNonHost);
rest = rest.substr(firstNonHost);
} else {
out.host = rest;
rest = "";
}
// pull out the auth and port.
var p = parseHost(out.host);
for (var i in p) out[i] = p[i];
// we've indicated that there is a hostname, so even if it's empty, it has to be present.
out.hostname = out.hostname || "";
}
// now rest is set to the post-host stuff.
// chop off from the tail first.
var hash = rest.indexOf("#");
if (hash !== -1) {
// got a fragment string.
out.hash = rest.substr(hash);
rest = rest.slice(0, hash);
}
var qm = rest.indexOf("?");
if (qm !== -1) {
out.search = rest.substr(qm);
out.query = rest.substr(qm+1);
if (parseQueryString) out.query = (querystring || querystring = require("querystring")).parse(out.query);
rest = rest.slice(0, qm);
}
if (rest) out.pathname = rest;
return out;
};
// format a parsed object into a url string
function url_format (obj) {
// ensure it's an object, and not a string url. If it's an obj, this is a no-op.
// this way, you can call url_format() on strings to clean up potentially wonky urls.
if (typeof(obj) === "string") obj = url_parse(obj);
var protocol = obj.protocol || "",
host = (obj.host !== undefined) ? obj.host
: obj.hostname !== undefined ? (
(obj.auth ? obj.auth + "@" : "")
+ obj.hostname
+ (obj.port ? ":" + obj.port : "")
)
: false,
pathname = obj.pathname || "",
search = obj.search || (
obj.query && ( "?" + (
typeof(obj.query) === "object"
? require("querystring").stringify(obj.query)
: String(obj.query)
))
) || "",
hash = obj.hash || "";
if (protocol && protocol.substr(-1) !== ":") protocol += ":";
// only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
// unless they had them to begin with.
if (obj.slashes || (!protocol || slashedProtocol[protocol]) && host !== false) {
host = "//" + (host || "");
if (pathname && pathname.charAt(0) !== "/") pathname = "/" + pathname;
} else if (!host) host = "";
if (hash && hash.charAt(0) !== "#") hash = "#" + hash;
if (search && search.charAt(0) !== "?") search = "?" + search;
return protocol + host + pathname + search + hash;
};
function url_resolve (source, relative) {
return url_format(url_resolveObject(source, relative));
};
function url_resolveObject (source, relative) {
if (!source) return relative;
source = url_parse(url_format(source));
relative = url_parse(url_format(relative));
// hash is always overridden, no matter what.
source.hash = relative.hash;
if (relative.href === "") return source;
// hrefs like //foo/bar always cut to the protocol.
if (relative.slashes && !relative.protocol) {
relative.protocol = source.protocol;
return relative;
}
if (relative.protocol && relative.protocol !== source.protocol) {
// if it's a known url protocol, then changing the protocol does weird things
// first, if it's not file:, then we MUST have a host, and if there was a path
// to begin with, then we MUST have a path.
// if it is file:, then the host is dropped, because that's known to be hostless.
// anything else is assumed to be absolute.
if (!slashedProtocol[relative.protocol]) return relative;
source.protocol = relative.protocol;
if (!relative.host && !hostlessProtocol[relative.protocol]) {
var relPath = (relative.pathname || "").split("/");
while (relPath.length && !(relative.host = relPath.shift()));
if (!relative.host) relative.host = "";
if (relPath[0] !== "") relPath.unshift("");
if (relPath.length < 2) relPath.unshift("");
relative.pathname = relPath.join("/");
}
source.pathname = relative.pathname;
source.search = relative.search;
source.query = relative.query;
source.host = relative.host || "";
delete source.auth;
delete source.hostname;
source.port = relative.port;
return source;
}
var isSourceAbs = (source.pathname && source.pathname.charAt(0) === "/"),
isRelAbs = (
relative.host !== undefined
|| relative.pathname && relative.pathname.charAt(0) === "/"
),
mustEndAbs = (isRelAbs || isSourceAbs || (source.host && relative.pathname)),
removeAllDots = mustEndAbs,
srcPath = source.pathname && source.pathname.split("/") || [],
relPath = relative.pathname && relative.pathname.split("/") || [],
psychotic = source.protocol && !slashedProtocol[source.protocol] && source.host !== undefined;
// if the url is a non-slashed url, then relative links like ../.. should be able
// to crawl up to the hostname, as well. This is strange.
// source.protocol has already been set by now.
// Later on, put the first path part into the host field.
if ( psychotic ) {
delete source.hostname;
delete source.auth;
delete source.port;
if (source.host) {
if (srcPath[0] === "") srcPath[0] = source.host;
else srcPath.unshift(source.host);
}
delete source.host;
if (relative.protocol) {
delete relative.hostname;
delete relative.auth;
delete relative.port;
if (relative.host) {
if (relPath[0] === "") relPath[0] = relative.host;
else relPath.unshift(relative.host);
}
delete relative.host;
}
mustEndAbs = mustEndAbs && (relPath[0] === "" || srcPath[0] === "");
}
if (isRelAbs) {
// it's absolute.
source.host = (relative.host || relative.host === "") ? relative.host : source.host;
source.search = relative.search;
source.query = relative.query;
srcPath = relPath;
// fall through to the dot-handling below.
} else if (relPath.length) {
// it's relative
// throw away the existing file, and take the new path instead.
if (!srcPath) srcPath = [];
srcPath.pop();
srcPath = srcPath.concat(relPath);
source.search = relative.search;
source.query = relative.query;
} else if ("search" in relative) {
// just pull out the search.
// like href="?foo".
// Put this after the other two cases because it simplifies the booleans
if (psychotic) {
source.host = srcPath.shift();
}
source.search = relative.search;
source.query = relative.query;
return source;
}
if (!srcPath.length) {
// no path at all. easy.
// we've already handled the other stuff above.
delete source.pathname;
return source;
}
// resolve dots.
// if a url ENDs in . or .., then it must get a trailing slash.
// however, if it ends in anything else non-slashy, then it must NOT get a trailing slash.
var last = srcPath.slice(-1)[0];
var hasTrailingSlash = (
(source.host || relative.host) && (last === "." || last === "..")
|| last === ""
);
// Figure out if this has to end up as an absolute url, or should continue to be relative.
srcPath = path.normalizeArray(srcPath, true);
if (srcPath.length === 1 && srcPath[0] === ".") srcPath = [];
if (mustEndAbs || removeAllDots) {
// all dots must go.
var dirs = [];
srcPath.forEach(function (dir, i) {
if (dir === "..") dirs.pop();
else if (dir !== ".") dirs.push(dir);
});
if (mustEndAbs && dirs[0] !== "") {
dirs.unshift("");
}
srcPath = dirs;
}
if (hasTrailingSlash && (srcPath.length < 2 || srcPath.slice(-1)[0] !== "")) srcPath.push("");
// put the host back
if ( psychotic ) source.host = srcPath[0] === "" ? "" : srcPath.shift();
mustEndAbs = mustEndAbs || (source.host && srcPath.length);
if (mustEndAbs && srcPath[0] !== "") srcPath.unshift("")
source.pathname = srcPath.join("/");
return source;
};
function parseHost (host) {
var out = {};
var at = host.indexOf("@");
if (at !== -1) {
out.auth = host.substr(0, at);
host = host.substr(at+1); // drop the @
}
var port = portPattern.exec(host);
if (port) {
port = port[0];
out.port = port.substr(1);
host = host.substr(0, host.length - port.length);
}
if (host) out.hostname = host;
return out;
}

125
test/mjsunit/test-querystring.js

@ -0,0 +1,125 @@
process.mixin(require("./common"));
// test using assert
var qs = require("querystring");
// folding block.
{
// [ wonkyQS, canonicalQS, obj ]
var qsTestCases = [
["foo=bar", "foo=bar", {"foo" : "bar"}],
["foo=bar&foo=quux", "foo%5B%5D=bar&foo%5B%5D=quux", {"foo" : ["bar", "quux"]}],
["foo=1&bar=2", "foo=1&bar=2", {"foo" : 1, "bar" : 2}],
["my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F", "my%20weird%20field=q1!2%22'w%245%267%2Fz8)%3F", {"my weird field" : "q1!2\"'w$5&7/z8)?" }],
["foo%3Dbaz=bar", "foo%3Dbaz=bar", {"foo=baz" : "bar"}],
["foo=baz=bar", "foo=baz%3Dbar", {"foo" : "baz=bar"}],
[ "str=foo&arr[]=1&arr[]=2&arr[]=3&obj[a]=bar&obj[b][]=4&obj[b][]=5&obj[b][]=6&obj[b][]=&obj[c][]=4&obj[c][]=5&obj[c][][somestr]=baz&obj[objobj][objobjstr]=blerg&somenull=&undef=", "str=foo&arr%5B%5D=1&arr%5B%5D=2&arr%5B%5D=3&obj%5Ba%5D=bar&obj%5Bb%5D%5B%5D=4&obj%5Bb%5D%5B%5D=5&obj%5Bb%5D%5B%5D=6&obj%5Bb%5D%5B%5D=&obj%5Bc%5D%5B%5D=4&obj%5Bc%5D%5B%5D=5&obj%5Bc%5D%5B%5D%5Bsomestr%5D=baz&obj%5Bobjobj%5D%5Bobjobjstr%5D=blerg&somenull=&undef=", {
"str":"foo",
"arr":[1,2,3],
"obj":{
"a":"bar",
"b":[4,5,6,""],
"c":[4,5,{"somestr":"baz"}],
"objobj":{"objobjstr":"blerg"}
},
"somenull":"",
"undef":""
}],
["foo[bar][bla]=baz&foo[bar][bla]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
["foo[bar][][bla]=baz&foo[bar][][bla]=blo", "foo%5Bbar%5D%5B%5D%5Bbla%5D=baz&foo%5Bbar%5D%5B%5D%5Bbla%5D=blo", {"foo":{"bar":[{"bla":"baz"},{"bla":"blo"}]}}],
["foo[bar][bla][]=baz&foo[bar][bla][]=blo", "foo%5Bbar%5D%5Bbla%5D%5B%5D=baz&foo%5Bbar%5D%5Bbla%5D%5B%5D=blo", {"foo":{"bar":{"bla":["baz","blo"]}}}],
[" foo = bar ", "foo=bar", {"foo":"bar"}]
];
// [ wonkyQS, canonicalQS, obj ]
var qsColonTestCases = [
["foo:bar", "foo:bar", {"foo":"bar"}],
["foo:bar;foo:quux", "foo%5B%5D:bar;foo%5B%5D:quux", {"foo" : ["bar", "quux"]}],
["foo:1&bar:2;baz:quux", "foo:1%26bar%3A2;baz:quux", {"foo":"1&bar:2", "baz":"quux"}],
["foo%3Abaz:bar", "foo%3Abaz:bar", {"foo:baz":"bar"}],
["foo:baz:bar", "foo:baz%3Abar", {"foo":"baz:bar"}]
];
// [ wonkyObj, qs, canonicalObj ]
var extendedFunction = function () {};
extendedFunction.prototype = {a:"b"};
var qsWeirdObjects = [
[ {regexp:/./g}, "regexp=", {"regexp":""} ],
[ {regexp: new RegExp(".", "g")}, "regexp=", {"regexp":""} ],
[ {fn:function () {}}, "fn=", {"fn":""}],
[ {fn:new Function("")}, "fn=", {"fn":""} ],
[ {math:Math}, "math=", {"math":""} ],
[ {e:extendedFunction}, "e=", {"e":""} ],
[ {d:new Date()}, "d=", {"d":""} ],
[ {d:Date}, "d=", {"d":""} ],
[ {f:new Boolean(false), t:new Boolean(true)}, "f=0&t=1", {"f":0, "t":1} ],
[ {f:false, t:true}, "f=0&t=1", {"f":0, "t":1} ],
];
}
// test that the canonical qs is parsed properly.
qsTestCases.forEach(function (testCase) {
assert.deepEqual(testCase[2], qs.parse(testCase[0]));
});
// test that the colon test cases can do the same
qsColonTestCases.forEach(function (testCase) {
assert.deepEqual(testCase[2], qs.parse(testCase[0], ";", ":"));
});
// test the weird objects, that they get parsed properly
qsWeirdObjects.forEach(function (testCase) {
assert.deepEqual(testCase[2], qs.parse(testCase[1]));
});
// test the nested qs-in-qs case
var f = qs.parse("a=b&q=x%3Dy%26y%3Dz");
f.q = qs.parse(f.q);
assert.deepEqual(f, { a : "b", q : { x : "y", y : "z" } });
// nested in colon
var f = qs.parse("a:b;q:x%3Ay%3By%3Az", ";", ":");
f.q = qs.parse(f.q, ";", ":");
assert.deepEqual(f, { a : "b", q : { x : "y", y : "z" } });
// now test stringifying
assert.throws(function () {
var f = {};
f.f = f;
qs.stringify(f);
});
// basic
qsTestCases.forEach(function (testCase) {
assert.equal(testCase[1], qs.stringify(testCase[2]));
});
qsColonTestCases.forEach(function (testCase) {
assert.equal(testCase[1], qs.stringify(testCase[2], ";", ":"));
});
qsWeirdObjects.forEach(function (testCase) {
assert.equal(testCase[1], qs.stringify(testCase[0]));
});
// nested
var f = qs.stringify({
a : "b",
q : qs.stringify({
x : "y",
y : "z"
})
});
assert.equal(f, "a=b&q=x%3Dy%26y%3Dz");
// nested in colon
var f = qs.stringify({
a : "b",
q : qs.stringify({
x : "y",
y : "z"
}, ";", ":")
}, ";", ":");
assert.equal(f, "a:b;q:x%3Ay%3By%3Az");

495
test/mjsunit/test-url.js

@ -0,0 +1,495 @@
process.mixin(require("./common"));
var url = require("url"),
sys = require("sys");
// URLs to parse, and expected data
// { url : parsed }
var parseTests = {
"http://www.narwhaljs.org/blog/categories?id=news" : {
"href": "http://www.narwhaljs.org/blog/categories?id=news",
"protocol": "http:",
"host": "www.narwhaljs.org",
"hostname": "www.narwhaljs.org",
"search": "?id=news",
"query": "id=news",
"pathname": "/blog/categories"
},
"http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=" : {
"href": "http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=",
"protocol": "http:",
"host": "mt0.google.com",
"hostname": "mt0.google.com",
"pathname": "/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s="
},
"http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=" : {
"href": "http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=",
"protocol": "http:",
"host": "mt0.google.com",
"hostname": "mt0.google.com",
"search": "???&hl=en&src=api&x=2&y=2&z=3&s=",
"query": "??&hl=en&src=api&x=2&y=2&z=3&s=",
"pathname": "/vt/lyrs=m@114"
},
"http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=" : {
"href": "http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=",
"protocol": "http:",
"host": "user:pass@mt0.google.com",
"auth": "user:pass",
"hostname": "mt0.google.com",
"search": "???&hl=en&src=api&x=2&y=2&z=3&s=",
"query": "??&hl=en&src=api&x=2&y=2&z=3&s=",
"pathname": "/vt/lyrs=m@114"
},
"file:///etc/passwd" : {
"href": "file:///etc/passwd",
"protocol": "file:",
"pathname": "///etc/passwd"
},
"file:///etc/node/" : {
"href": "file:///etc/node/",
"protocol": "file:",
"pathname": "///etc/node/"
},
"http:/baz/../foo/bar" : {
"href": "http:/baz/../foo/bar",
"protocol": "http:",
"pathname": "/baz/../foo/bar"
},
"http://user:pass@example.com:8000/foo/bar?baz=quux#frag" : {
"href": "http://user:pass@example.com:8000/foo/bar?baz=quux#frag",
"protocol": "http:",
"host": "user:pass@example.com:8000",
"auth": "user:pass",
"port": "8000",
"hostname": "example.com",
"hash": "#frag",
"search": "?baz=quux",
"query": "baz=quux",
"pathname": "/foo/bar"
},
"//user:pass@example.com:8000/foo/bar?baz=quux#frag" : {
"href": "//user:pass@example.com:8000/foo/bar?baz=quux#frag",
"host": "user:pass@example.com:8000",
"auth": "user:pass",
"port": "8000",
"hostname": "example.com",
"hash": "#frag",
"search": "?baz=quux",
"query": "baz=quux",
"pathname": "/foo/bar"
},
"http://example.com?foo=bar#frag" : {
"href": "http://example.com?foo=bar#frag",
"protocol": "http:",
"host": "example.com",
"hostname": "example.com",
"hash": "#frag",
"search": "?foo=bar",
"query": "foo=bar"
},
"http://example.com?foo=@bar#frag" : {
"href": "http://example.com?foo=@bar#frag",
"protocol": "http:",
"host": "example.com",
"hostname": "example.com",
"hash": "#frag",
"search": "?foo=@bar",
"query": "foo=@bar"
},
"http://example.com?foo=/bar/#frag" : {
"href": "http://example.com?foo=/bar/#frag",
"protocol": "http:",
"host": "example.com",
"hostname": "example.com",
"hash": "#frag",
"search": "?foo=/bar/",
"query": "foo=/bar/"
},
"http://example.com?foo=?bar/#frag" : {
"href": "http://example.com?foo=?bar/#frag",
"protocol": "http:",
"host": "example.com",
"hostname": "example.com",
"hash": "#frag",
"search": "?foo=?bar/",
"query": "foo=?bar/"
},
"http://example.com#frag=?bar/#frag" : {
"href": "http://example.com#frag=?bar/#frag",
"protocol": "http:",
"host": "example.com",
"hostname": "example.com",
"hash": "#frag=?bar/#frag"
},
"/foo/bar?baz=quux#frag" : {
"href": "/foo/bar?baz=quux#frag",
"hash": "#frag",
"search": "?baz=quux",
"query": "baz=quux",
"pathname": "/foo/bar"
},
"http:/foo/bar?baz=quux#frag" : {
"href": "http:/foo/bar?baz=quux#frag",
"protocol": "http:",
"hash": "#frag",
"search": "?baz=quux",
"query": "baz=quux",
"pathname": "/foo/bar"
},
"mailto:foo@bar.com?subject=hello" : {
"href": "mailto:foo@bar.com?subject=hello",
"protocol": "mailto:",
"host": "foo@bar.com",
"auth" : "foo",
"hostname" : "bar.com",
"search": "?subject=hello",
"query": "subject=hello"
},
"javascript:alert('hello');" : {
"href": "javascript:alert('hello');",
"protocol": "javascript:",
"host": "alert('hello')",
"hostname": "alert('hello')",
"pathname" : ";"
},
"xmpp:isaacschlueter@jabber.org" : {
"href": "xmpp:isaacschlueter@jabber.org",
"protocol": "xmpp:",
"host": "isaacschlueter@jabber.org",
"auth": "isaacschlueter",
"hostname": "jabber.org"
}
};
for (var u in parseTests) {
var actual = url.parse(u),
expected = parseTests[u];
for (var i in expected) {
var e = JSON.stringify(expected[i]),
a = JSON.stringify(actual[i]);
assert.equal(e, a, "parse(" + u + ")."+i+" == "+e+"\nactual: "+a);
}
var expected = u,
actual = url.format(parseTests[u]);
assert.equal(expected, actual, "format("+u+") == "+u+"\nactual:"+actual);
}
// some extra formatting tests, just to verify that it'll format slightly wonky content to a valid url.
var formatTests = {
"http://a.com/a/b/c?s#h" : {
"protocol": "http",
"host": "a.com",
"pathname": "a/b/c",
"hash": "h",
"search": "s"
},
"xmpp:isaacschlueter@jabber.org" : {
"href": "xmpp://isaacschlueter@jabber.org",
"protocol": "xmpp:",
"host": "isaacschlueter@jabber.org",
"auth": "isaacschlueter",
"hostname": "jabber.org"
}
};
for (var u in formatTests) {
var actual = url.format(formatTests[u]);
assert.equal(actual, u, "wonky format("+u+") == "+u+"\nactual:"+actual);
}
[
// [from, path, expected]
["/foo/bar/baz", "quux", "/foo/bar/quux"],
["/foo/bar/baz", "quux/asdf", "/foo/bar/quux/asdf"],
["/foo/bar/baz", "quux/baz", "/foo/bar/quux/baz"],
["/foo/bar/baz", "../quux/baz", "/foo/quux/baz"],
["/foo/bar/baz", "/bar", "/bar"],
["/foo/bar/baz/", "quux", "/foo/bar/baz/quux"],
["/foo/bar/baz/", "quux/baz", "/foo/bar/baz/quux/baz"],
["/foo/bar/baz", "../../../../../../../../quux/baz", "/quux/baz"],
["/foo/bar/baz", "../../../../../../../quux/baz", "/quux/baz"],
["foo/bar", "../../../baz", "../../baz"],
["foo/bar/", "../../../baz", "../baz"],
["http://example.com/b//c//d;p?q#blarg","https:#hash2","https:///#hash2" ],
["http://example.com/b//c//d;p?q#blarg","https:/p/a/t/h?s#hash2","https://p/a/t/h?s#hash2" ],
["http://example.com/b//c//d;p?q#blarg","https://u:p@h.com/p/a/t/h?s#hash2","https://u:p@h.com/p/a/t/h?s#hash2"],
["http://example.com/b//c//d;p?q#blarg","https:/a/b/c/d","https://a/b/c/d"],
["http://example.com/b//c//d;p?q#blarg","http:#hash2","http://example.com/b//c//d;p?q#hash2" ],
["http://example.com/b//c//d;p?q#blarg","http:/p/a/t/h?s#hash2","http://example.com/p/a/t/h?s#hash2" ],
["http://example.com/b//c//d;p?q#blarg","http://u:p@h.com/p/a/t/h?s#hash2","http://u:p@h.com/p/a/t/h?s#hash2" ],
["http://example.com/b//c//d;p?q#blarg","http:/a/b/c/d","http://example.com/a/b/c/d"],
["/foo/bar/baz", "/../etc/passwd", "/etc/passwd"]
].forEach(function (relativeTest) {
var a = url.resolve(relativeTest[0], relativeTest[1]),
e = relativeTest[2];
assert.equal(e, a,
"resolve("+[relativeTest[0], relativeTest[1]]+") == "+e+
"\n actual="+a);
});
//
// Tests below taken from Chiron
// http://code.google.com/p/chironjs/source/browse/trunk/src/test/http/url.js
//
// Copyright (c) 2002-2008 Kris Kowal <http://cixar.com/~kris.kowal>
// used with permission under MIT License
//
// Changes marked with @isaacs
var bases = [
'http://a/b/c/d;p?q',
'http://a/b/c/d;p?q=1/2',
'http://a/b/c/d;p=1/2?q',
'fred:///s//a/b/c',
'http:///s//a/b/c'
];
//[to, from, result]
[
// http://lists.w3.org/Archives/Public/uri/2004Feb/0114.html
['../c', 'foo:a/b', 'foo:c'],
['foo:.', 'foo:a', 'foo:'],
['/foo/../../../bar', 'zz:abc', 'zz:/bar'],
['/foo/../bar', 'zz:abc', 'zz:/bar'],
['foo/../../../bar', 'zz:abc', 'zz:bar'], // @isaacs Disagree. Not how web browsers resolve this.
// ['foo/../../../bar', 'zz:abc', 'zz:../../bar'], // @isaacs Added
['foo/../bar', 'zz:abc', 'zz:bar'],
['zz:.', 'zz:abc', 'zz:'],
['/.' , bases[0], 'http://a/'],
['/.foo' , bases[0], 'http://a/.foo'],
['.foo' , bases[0], 'http://a/b/c/.foo'],
// http://gbiv.com/protocols/uri/test/rel_examples1.html
// examples from RFC 2396
['g:h' , bases[0], 'g:h'],
['g' , bases[0], 'http://a/b/c/g'],
['./g' , bases[0], 'http://a/b/c/g'],
['g/' , bases[0], 'http://a/b/c/g/'],
['/g' , bases[0], 'http://a/g'],
['//g' , bases[0], 'http://g'],
// changed with RFC 2396bis
//('?y' , bases[0], 'http://a/b/c/d;p?y'],
['?y' , bases[0], 'http://a/b/c/d;p?y'],
['g?y' , bases[0], 'http://a/b/c/g?y'],
// changed with RFC 2396bis
//('#s' , bases[0], CURRENT_DOC_URI + '#s'],
['#s' , bases[0], 'http://a/b/c/d;p?q#s'],
['g#s' , bases[0], 'http://a/b/c/g#s'],
['g?y#s' , bases[0], 'http://a/b/c/g?y#s'],
[';x' , bases[0], 'http://a/b/c/;x'],
['g;x' , bases[0], 'http://a/b/c/g;x'],
['g;x?y#s' , bases[0], 'http://a/b/c/g;x?y#s'],
// changed with RFC 2396bis
//('' , bases[0], CURRENT_DOC_URI],
['' , bases[0], 'http://a/b/c/d;p?q'],
['.' , bases[0], 'http://a/b/c/'],
['./' , bases[0], 'http://a/b/c/'],
['..' , bases[0], 'http://a/b/'],
['../' , bases[0], 'http://a/b/'],
['../g' , bases[0], 'http://a/b/g'],
['../..' , bases[0], 'http://a/'],
['../../' , bases[0], 'http://a/'],
['../../g' , bases[0], 'http://a/g'],
['../../../g', bases[0], ('http://a/../g', 'http://a/g')],
['../../../../g', bases[0], ('http://a/../../g', 'http://a/g')],
// changed with RFC 2396bis
//('/./g', bases[0], 'http://a/./g'],
['/./g', bases[0], 'http://a/g'],
// changed with RFC 2396bis
//('/../g', bases[0], 'http://a/../g'],
['/../g', bases[0], 'http://a/g'],
['g.', bases[0], 'http://a/b/c/g.'],
['.g', bases[0], 'http://a/b/c/.g'],
['g..', bases[0], 'http://a/b/c/g..'],
['..g', bases[0], 'http://a/b/c/..g'],
['./../g', bases[0], 'http://a/b/g'],
['./g/.', bases[0], 'http://a/b/c/g/'],
['g/./h', bases[0], 'http://a/b/c/g/h'],
['g/../h', bases[0], 'http://a/b/c/h'],
['g;x=1/./y', bases[0], 'http://a/b/c/g;x=1/y'],
['g;x=1/../y', bases[0], 'http://a/b/c/y'],
['g?y/./x', bases[0], 'http://a/b/c/g?y/./x'],
['g?y/../x', bases[0], 'http://a/b/c/g?y/../x'],
['g#s/./x', bases[0], 'http://a/b/c/g#s/./x'],
['g#s/../x', bases[0], 'http://a/b/c/g#s/../x'],
['http:g', bases[0], ('http:g', 'http://a/b/c/g')],
['http:', bases[0], ('http:', bases[0])],
// not sure where this one originated
['/a/b/c/./../../g', bases[0], 'http://a/a/g'],
// http://gbiv.com/protocols/uri/test/rel_examples2.html
// slashes in base URI's query args
['g' , bases[1], 'http://a/b/c/g'],
['./g' , bases[1], 'http://a/b/c/g'],
['g/' , bases[1], 'http://a/b/c/g/'],
['/g' , bases[1], 'http://a/g'],
['//g' , bases[1], 'http://g'],
// changed in RFC 2396bis
//('?y' , bases[1], 'http://a/b/c/?y'],
['?y' , bases[1], 'http://a/b/c/d;p?y'],
['g?y' , bases[1], 'http://a/b/c/g?y'],
['g?y/./x' , bases[1], 'http://a/b/c/g?y/./x'],
['g?y/../x', bases[1], 'http://a/b/c/g?y/../x'],
['g#s' , bases[1], 'http://a/b/c/g#s'],
['g#s/./x' , bases[1], 'http://a/b/c/g#s/./x'],
['g#s/../x', bases[1], 'http://a/b/c/g#s/../x'],
['./' , bases[1], 'http://a/b/c/'],
['../' , bases[1], 'http://a/b/'],
['../g' , bases[1], 'http://a/b/g'],
['../../' , bases[1], 'http://a/'],
['../../g' , bases[1], 'http://a/g'],
// http://gbiv.com/protocols/uri/test/rel_examples3.html
// slashes in path params
// all of these changed in RFC 2396bis
['g' , bases[2], 'http://a/b/c/d;p=1/g'],
['./g' , bases[2], 'http://a/b/c/d;p=1/g'],
['g/' , bases[2], 'http://a/b/c/d;p=1/g/'],
['g?y' , bases[2], 'http://a/b/c/d;p=1/g?y'],
[';x' , bases[2], 'http://a/b/c/d;p=1/;x'],
['g;x' , bases[2], 'http://a/b/c/d;p=1/g;x'],
['g;x=1/./y', bases[2], 'http://a/b/c/d;p=1/g;x=1/y'],
['g;x=1/../y', bases[2], 'http://a/b/c/d;p=1/y'],
['./' , bases[2], 'http://a/b/c/d;p=1/'],
['../' , bases[2], 'http://a/b/c/'],
['../g' , bases[2], 'http://a/b/c/g'],
['../../' , bases[2], 'http://a/b/'],
['../../g' , bases[2], 'http://a/b/g'],
// http://gbiv.com/protocols/uri/test/rel_examples4.html
// double and triple slash, unknown scheme
['g:h' , bases[3], 'g:h'],
['g' , bases[3], 'fred:///s//a/b/g'],
['./g' , bases[3], 'fred:///s//a/b/g'],
['g/' , bases[3], 'fred:///s//a/b/g/'],
['/g' , bases[3], 'fred:///g'], // may change to fred:///s//a/g
['//g' , bases[3], 'fred://g'], // may change to fred:///s//g
['//g/x' , bases[3], 'fred://g/x'], // may change to fred:///s//g/x
['///g' , bases[3], 'fred:///g'],
['./' , bases[3], 'fred:///s//a/b/'],
['../' , bases[3], 'fred:///s//a/'],
['../g' , bases[3], 'fred:///s//a/g'],
['../../' , bases[3], 'fred:///s//'],
['../../g' , bases[3], 'fred:///s//g'],
['../../../g', bases[3], 'fred:///s/g'],
['../../../../g', bases[3], 'fred:///g'], // may change to fred:///s//a/../../../g
// http://gbiv.com/protocols/uri/test/rel_examples5.html
// double and triple slash, well-known scheme
['g:h' , bases[4], 'g:h'],
['g' , bases[4], 'http:///s//a/b/g'],
['./g' , bases[4], 'http:///s//a/b/g'],
['g/' , bases[4], 'http:///s//a/b/g/'],
['/g' , bases[4], 'http:///g'], // may change to http:///s//a/g
['//g' , bases[4], 'http://g'], // may change to http:///s//g
['//g/x' , bases[4], 'http://g/x'], // may change to http:///s//g/x
['///g' , bases[4], 'http:///g'],
['./' , bases[4], 'http:///s//a/b/'],
['../' , bases[4], 'http:///s//a/'],
['../g' , bases[4], 'http:///s//a/g'],
['../../' , bases[4], 'http:///s//'],
['../../g' , bases[4], 'http:///s//g'],
['../../../g', bases[4], 'http:///s/g'], // may change to http:///s//a/../../g
['../../../../g', bases[4], 'http:///g'], // may change to http:///s//a/../../../g
// from Dan Connelly's tests in http://www.w3.org/2000/10/swap/uripath.py
["bar:abc", "foo:xyz", "bar:abc"],
['../abc', 'http://example/x/y/z', 'http://example/x/abc'],
['http://example/x/abc', 'http://example2/x/y/z', 'http://example/x/abc'],
['../r', 'http://ex/x/y/z', 'http://ex/x/r'],
['q/r', 'http://ex/x/y', 'http://ex/x/q/r'],
['q/r#s', 'http://ex/x/y', 'http://ex/x/q/r#s'],
['q/r#s/t', 'http://ex/x/y', 'http://ex/x/q/r#s/t'],
['ftp://ex/x/q/r', 'http://ex/x/y', 'ftp://ex/x/q/r'],
['', 'http://ex/x/y', 'http://ex/x/y'],
['', 'http://ex/x/y/', 'http://ex/x/y/'],
['', 'http://ex/x/y/pdq', 'http://ex/x/y/pdq'],
['z/', 'http://ex/x/y/', 'http://ex/x/y/z/'],
['#Animal', 'file:/swap/test/animal.rdf', 'file:/swap/test/animal.rdf#Animal'],
['../abc', 'file:/e/x/y/z', 'file:/e/x/abc'],
['/example/x/abc', 'file:/example2/x/y/z', 'file:/example/x/abc'],
['../r', 'file:/ex/x/y/z', 'file:/ex/x/r'],
['/r', 'file:/ex/x/y/z', 'file:/r'],
['q/r', 'file:/ex/x/y', 'file:/ex/x/q/r'],
['q/r#s', 'file:/ex/x/y', 'file:/ex/x/q/r#s'],
['q/r#', 'file:/ex/x/y', 'file:/ex/x/q/r#'],
['q/r#s/t', 'file:/ex/x/y', 'file:/ex/x/q/r#s/t'],
['ftp://ex/x/q/r', 'file:/ex/x/y', 'ftp://ex/x/q/r'],
['', 'file:/ex/x/y', 'file:/ex/x/y'],
['', 'file:/ex/x/y/', 'file:/ex/x/y/'],
['', 'file:/ex/x/y/pdq', 'file:/ex/x/y/pdq'],
['z/', 'file:/ex/x/y/', 'file:/ex/x/y/z/'],
['file://meetings.example.com/cal#m1', 'file:/devel/WWW/2000/10/swap/test/reluri-1.n3', 'file://meetings.example.com/cal#m1'],
['file://meetings.example.com/cal#m1', 'file:/home/connolly/w3ccvs/WWW/2000/10/swap/test/reluri-1.n3', 'file://meetings.example.com/cal#m1'],
['./#blort', 'file:/some/dir/foo', 'file:/some/dir/#blort'],
['./#', 'file:/some/dir/foo', 'file:/some/dir/#'],
// Ryan Lee
["./", "http://example/x/abc.efg", "http://example/x/"],
// Graham Klyne's tests
// http://www.ninebynine.org/Software/HaskellUtils/Network/UriTest.xls
// 01-31 are from Connelly's cases
// 32-49
['./q:r', 'http://ex/x/y', 'http://ex/x/q:r'],
['./p=q:r', 'http://ex/x/y', 'http://ex/x/p=q:r'],
['?pp/rr', 'http://ex/x/y?pp/qq', 'http://ex/x/y?pp/rr'],
['y/z', 'http://ex/x/y?pp/qq', 'http://ex/x/y/z'],
['local/qual@domain.org#frag', 'mailto:local', 'mailto:local/qual@domain.org#frag'],
['more/qual2@domain2.org#frag', 'mailto:local/qual1@domain1.org', 'mailto:local/more/qual2@domain2.org#frag'],
['y?q', 'http://ex/x/y?q', 'http://ex/x/y?q'],
['/x/y?q', 'http://ex?p', 'http://ex/x/y?q'],
['c/d', 'foo:a/b', 'foo:a/c/d'],
['/c/d', 'foo:a/b', 'foo:/c/d'],
['', 'foo:a/b?c#d', 'foo:a/b?c'],
['b/c', 'foo:a', 'foo:b/c'],
['../b/c', 'foo:/a/y/z', 'foo:/a/b/c'],
['./b/c', 'foo:a', 'foo:b/c'],
['/./b/c', 'foo:a', 'foo:/b/c'],
['../../d', 'foo://a//b/c', 'foo://a/d'],
['.', 'foo:a', 'foo:'],
['..', 'foo:a', 'foo:'],
// 50-57[cf. TimBL comments --
// http://lists.w3.org/Archives/Public/uri/2003Feb/0028.html,
// http://lists.w3.org/Archives/Public/uri/2003Jan/0008.html)
['abc', 'http://example/x/y%2Fz', 'http://example/x/abc'],
['../../x%2Fabc', 'http://example/a/x/y/z', 'http://example/a/x%2Fabc'],
['../x%2Fabc', 'http://example/a/x/y%2Fz', 'http://example/a/x%2Fabc'],
['abc', 'http://example/x%2Fy/z', 'http://example/x%2Fy/abc'],
['q%3Ar', 'http://ex/x/y', 'http://ex/x/q%3Ar'],
['/x%2Fabc', 'http://example/x/y%2Fz', 'http://example/x%2Fabc'],
['/x%2Fabc', 'http://example/x/y/z', 'http://example/x%2Fabc'],
['/x%2Fabc', 'http://example/x/y%2Fz', 'http://example/x%2Fabc'],
// 70-77
['local2@domain2', 'mailto:local1@domain1?query1', 'mailto:local2@domain2'],
['local2@domain2?query2', 'mailto:local1@domain1', 'mailto:local2@domain2?query2'],
['local2@domain2?query2', 'mailto:local1@domain1?query1', 'mailto:local2@domain2?query2'],
['?query2', 'mailto:local@domain?query1', 'mailto:local@domain?query2'],
['local@domain?query2', 'mailto:?query1', 'mailto:local@domain?query2'],
['?query2', 'mailto:local@domain?query1', 'mailto:local@domain?query2'],
['http://example/a/b?c/../d', 'foo:bar', 'http://example/a/b?c/../d'],
['http://example/a/b#c/../d', 'foo:bar', 'http://example/a/b#c/../d'],
// 82-88
// ['http:this', 'http://example.org/base/uri', 'http:this'], // @isaacs Disagree. Not how browsers do it.
['http:this', 'http://example.org/base/uri', "http://example.org/base/this"], // @isaacs Added
['http:this', 'http:base', 'http:this'],
['.//g', 'f:/a', 'f://g'],
['b/c//d/e', 'f://example.org/base/a', 'f://example.org/base/b/c//d/e'],
['m2@example.ord/c2@example.org', 'mid:m@example.ord/c@example.org', 'mid:m@example.ord/m2@example.ord/c2@example.org'],
['mini1.xml', 'file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/', 'file:///C:/DEV/Haskell/lib/HXmlToolbox-3.01/examples/mini1.xml'],
['../b/c', 'foo:a/y/z', 'foo:a/b/c']
].forEach(function (relativeTest) {
var a = url.resolve(relativeTest[1], relativeTest[0]),
e = relativeTest[2];
assert.equal(e, a,
"resolve("+[relativeTest[1], relativeTest[0]]+") == "+e+
"\n actual="+a);
});
Loading…
Cancel
Save