Skip to content

Commit

Permalink
Make navigation work more like spec
Browse files Browse the repository at this point in the history
The main impacts of this, besides refactoring, are:

* Implements navigation to javascript: URLs that do not return strings (i.e. that only cause side effects)
* Updates whatwg-url to 6.1.0 (fixes #1870)
  • Loading branch information
ForbesLindesay authored and domenic committed Jul 3, 2017
1 parent 2562b8e commit b51255b
Show file tree
Hide file tree
Showing 14 changed files with 107 additions and 61 deletions.
4 changes: 2 additions & 2 deletions lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ class JSDOM {
const document = idlUtils.implForWrapper(this[window]._document);

const url = whatwgURL.parseURL(settings.url);
if (url === "failure") {
if (url === null) {
throw new TypeError(`Could not parse "${settings.url}" as a URL`);
}

document._URL = url;
document._origin = whatwgURL.serializeURLToUnicodeOrigin(document._URL);
document._origin = whatwgURL.serializeURLOrigin(document._URL);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/helpers/document-base-url.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ function frozenBaseURL(baseElement, fallbackBaseURL) {

const baseHrefAttribute = baseElement.getAttribute("href");
const result = whatwgURL.parseURL(baseHrefAttribute, { baseURL: fallbackBaseURL });
return result === "failure" ? fallbackBaseURL : result;
return result === null ? fallbackBaseURL : result;
}
2 changes: 1 addition & 1 deletion lib/jsdom/living/helpers/stylesheets.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function scanForImportRules(elementImpl, cssRules, baseURL) {
// If loading of the style sheet fails its cssRules list is simply
// empty. I.e. an @import rule always has an associated style sheet.
const parsed = whatwgURL.parseURL(cssRules[i].href, { baseURL });
if (parsed === "failure") {
if (parsed === null) {
const window = elementImpl._ownerDocument._defaultView;
if (window) {
const error = new Error(`Could not parse CSS @import URL ${cssRules[i].href} relative to base URL ` +
Expand Down
4 changes: 2 additions & 2 deletions lib/jsdom/living/nodes/Document-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ class DocumentImpl extends NodeImpl {

const urlOption = privateData.options.url === undefined ? "about:blank" : privateData.options.url;
const parsed = whatwgURL.parseURL(urlOption);
if (parsed === "failure") {
if (parsed === null) {
throw new TypeError(`Could not parse "${urlOption}" as a URL`);
}

this._URL = parsed;
this._origin = whatwgURL.serializeURLToUnicodeOrigin(parsed);
this._origin = whatwgURL.serializeURLOrigin(parsed);

this._location = Location.createImpl([], { relevantDocument: this });
this._history = History.createImpl([], {
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/nodes/HTMLBaseElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class HTMLBaseElement extends HTMLElementImpl {
const url = this.hasAttribute("href") ? this.getAttribute("href") : "";
const parsed = whatwgURL.parseURL(url, { baseURL: fallbackBaseURL(document) });

if (parsed === "failure") {
if (parsed === null) {
return url;
}

Expand Down
4 changes: 2 additions & 2 deletions lib/jsdom/living/nodes/HTMLHyperlinkElementUtils-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ exports.implementation = class HTMLHyperlinkElementUtilsImpl {
return "";
}

return whatwgURL.serializeURLToUnicodeOrigin(this.url);
return whatwgURL.serializeURLOrigin(this.url);
}

get protocol() {
Expand Down Expand Up @@ -288,7 +288,7 @@ function setTheURL(hheu) {

const parsed = parseURLToResultingURLRecord(href, hheu._ownerDocument);

hheu.url = parsed === "failure" ? null : parsed;
hheu.url = parsed === null ? null : parsed;
}

function updateHref(hheu) {
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/nodes/HTMLLinkElement-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function obtainTheResource(el) {
}

const url = parseURLToResultingURLRecord(href, el._ownerDocument);
if (url === "failure") {
if (url === null) {
return;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/window/History-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ exports.implementation = class HistoryImpl {
// difference matters in the case of cross-frame calls.
newURL = parseURLToResultingURLRecord(url, this._document);

if (newURL === "failure") {
if (newURL === null) {
throw new DOMException(DOMException.SECURITY_ERR, `Could not parse url argument "${url}" to ${methodName} ` +
`against base URL "${documentBaseURLSerialized(this._document)}".`);
}
Expand Down
22 changes: 10 additions & 12 deletions lib/jsdom/living/window/Location-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const whatwgURL = require("whatwg-url");
const documentBaseURL = require("../helpers/document-base-url.js").documentBaseURL;
const parseURLToResultingURLRecord = require("../helpers/document-base-url.js").parseURLToResultingURLRecord;
const DOMException = require("../../web-idl/DOMException.js");
const notImplemented = require("../../browser/not-implemented.js");
const navigate = require("./navigation.js").navigate;

// Not implemented: use of entry settings object's API base URL in href setter, assign, and replace. Instead we just
Expand All @@ -20,17 +19,15 @@ exports.implementation = class LocationImpl {
}

_locationObjectSetterNavigate(url) {
// Not implemented: extra steps here to determine replacement flag, since they are not applicable to our
// rudimentary "navigation" implementation.
// Not implemented: extra steps here to determine replacement flag.

return this._locationObjectNavigate(url);
}

_locationObjectNavigate(url/* , { replacement = false } = {} */) {
_locationObjectNavigate(url, { replacement = false } = {}) {
// Not implemented: the setup for calling navigate, which doesn't apply to our stub navigate anyway.
// Not implemented: using the replacement flag.

navigate(this._relevantDocument._defaultView, url);
navigate(this._relevantDocument._defaultView, url, { replacement, exceptionsEnabled: true });
}

toString() {
Expand All @@ -42,15 +39,15 @@ exports.implementation = class LocationImpl {
}
set href(v) {
const newURL = whatwgURL.parseURL(v, { baseURL: documentBaseURL(this._relevantDocument) });
if (newURL === "failure") {
if (newURL === null) {
throw new TypeError(`Could not parse "${v}" as a URL`);
}

this._locationObjectSetterNavigate(newURL);
}

get origin() {
return whatwgURL.serializeURLToUnicodeOrigin(this._url);
return whatwgURL.serializeURLOrigin(this._url);
}

get protocol() {
Expand All @@ -60,7 +57,7 @@ exports.implementation = class LocationImpl {
const copyURL = Object.assign({}, this._url);

const possibleFailure = whatwgURL.basicURLParse(v + ":", { url: copyURL, stateOverride: "scheme start" });
if (possibleFailure === "failure") {
if (possibleFailure === null) {
throw new TypeError(`Could not parse the URL after setting the procol to "${v}"`);
}

Expand Down Expand Up @@ -209,7 +206,7 @@ exports.implementation = class LocationImpl {
// Should be entry settings object; oh well
const parsedURL = parseURLToResultingURLRecord(url, this._relevantDocument);

if (parsedURL === "failure") {
if (parsedURL === null) {
throw new DOMException(DOMException.SYNTAX_ERR, `Could not resolve the given string "${url}" relative to the ` +
`base URL "${this._relevantDocument.URL}"`);
}
Expand All @@ -221,7 +218,7 @@ exports.implementation = class LocationImpl {
// Should be entry settings object; oh well
const parsedURL = parseURLToResultingURLRecord(url, this._relevantDocument);

if (parsedURL === "failure") {
if (parsedURL === null) {
throw new DOMException(DOMException.SYNTAX_ERR, `Could not resolve the given string "${url}" relative to the ` +
`base URL "${this._relevantDocument.URL}"`);
}
Expand All @@ -230,6 +227,7 @@ exports.implementation = class LocationImpl {
}

reload() {
notImplemented("location.reload()", this._relevantDocument._defaultView);
const flags = { replace: true, reloadTriggered: true, exceptionsEnabled: true };
navigate(this._relevantDocument._defaultView, this._url, flags);
}
};
96 changes: 73 additions & 23 deletions lib/jsdom/living/window/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@ const HashChangeEvent = require("../generated/HashChangeEvent.js");
const PopStateEvent = require("../generated/PopStateEvent.js");
const idlUtils = require("../generated/utils");

exports.traverseHistory = (window, specifiedEntry, flags) => {
exports.traverseHistory = (window, specifiedEntry, flags = {}) => {
// Not spec compliant, just minimal. Lots of missing steps.

if (flags === undefined) {
flags = {};
}
const nonBlockingEvents = Boolean(flags.nonBlockingEvents);

const document = idlUtils.implForWrapper(window._document);
Expand Down Expand Up @@ -67,37 +64,90 @@ exports.traverseHistory = (window, specifiedEntry, flags) => {
window._currentSessionHistoryEntryIndex = window._sessionHistory.indexOf(specifiedEntry);
};

exports.navigate = (window, newURL) => {
// https://html.spec.whatwg.org/#navigating-across-documents
exports.navigate = (window, newURL, flags) => {
// This is NOT a spec-compliant implementation of navigation in any way. It implements a few selective steps that
// are nice for jsdom users, regarding hash changes. We could maybe implement javascript: URLs in the future, but
// the rest is too hard.
// are nice for jsdom users, regarding hash changes and JavaScript URLs. Full navigation support is being worked on
// and will likely require some additional hooks to be implemented.

const document = idlUtils.implForWrapper(window._document);

const currentURL = document._URL;

if (newURL.scheme !== currentURL.scheme ||
newURL.username !== currentURL.username ||
newURL.password !== currentURL.password ||
newURL.host !== currentURL.host ||
newURL.port !== currentURL.port ||
!arrayEqual(newURL.path, currentURL.path) ||
newURL.query !== currentURL.query ||
// Omitted per spec: url.fragment !== this._url.fragment ||
newURL.cannotBeABaseURL !== currentURL.cannotBeABaseURL) {
notImplemented("navigation (except hash changes)", window);
if (!flags.reloadTriggered && urlEquals(currentURL, newURL, { excludeFragments: true })) {
if (newURL.fragment !== currentURL.fragment) {
navigateToFragment(window, newURL, flags);
}
return;
}

if (newURL.fragment !== currentURL.fragment) {
// https://html.spec.whatwg.org/#scroll-to-fragid
// NOT IMPLEMENTED: Prompt to unload the active document of browsingContext.

// NOT IMPLEMENTED: form submission algorithm
// const navigationType = 'other';

// NOT IMPLEMENTED: if resource is a response...
if (newURL.scheme === "javascript") {
window.setTimeout(() => {
const urlString = whatwgURL.serializeURL(newURL).replace(/^javascript:/, "");
// https://url.spec.whatwg.org/#percent-decode
const scriptSource = whatwgURL.percentDecode(Buffer.from(urlString)).toString();
if (window._runScripts === "dangerously") {
const result = window.eval(scriptSource);
if (typeof result === "string") {
notImplemented("string results from 'javascript:' URLs", window);
}
}
}, 0);
return;
}
navigateFetch(window);
};

window._sessionHistory.splice(window._currentSessionHistoryEntryIndex + 1, Infinity);
// https://html.spec.whatwg.org/#scroll-to-fragid
function navigateToFragment(window, newURL, flags) {
const document = idlUtils.implForWrapper(window._document);

document._history._clearHistoryTraversalTasks();
document._history._clearHistoryTraversalTasks();

if (!flags.replace) {
window._sessionHistory.splice(window._currentSessionHistoryEntryIndex + 1, Infinity);
const newEntry = { document, url: newURL };
window._sessionHistory.push(newEntry);
exports.traverseHistory(window, newEntry, { nonBlockingEvents: true });
} else {
// handling replace=true here deviates from spec, but matches real browser behaviour
// see https://github.com/whatwg/html/issues/2796 for spec bug
const currentEntry = window._sessionHistory[window._currentSessionHistoryEntryIndex];
const oldURL = currentEntry.url;
currentEntry.url = newURL;
window.setTimeout(() => {
window.dispatchEvent(HashChangeEvent.create(["hashchange", {
bubbles: true,
cancelable: false,
oldURL: whatwgURL.serializeURL(oldURL),
newURL: whatwgURL.serializeURL(newURL)
}]));
}, 0);
}
};
}

// https://html.spec.whatwg.org/#process-a-navigate-fetch
function navigateFetch(window) {
// TODO:
notImplemented("navigation (except hash changes)", window);
}

function urlEquals(a, b, flags) {
if (a.scheme !== b.scheme ||
a.username !== b.username ||
a.password !== b.password ||
a.host !== b.host ||
a.port !== b.port ||
!arrayEqual(a.path, b.path) ||
a.query !== b.query ||
// Omitted per spec: url.fragment !== this._url.fragment ||
a.cannotBeABaseURL !== b.cannotBeABaseURL) {
return false;
}
return flags.excludeFragments || a.fragment === b.fragment;
}
4 changes: 2 additions & 2 deletions lib/jsdom/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ exports.reflectURLAttribute = (elementImpl, contentAttributeName) => {
}

const urlRecord = parseURLToResultingURLRecord(attributeValue, elementImpl._ownerDocument);
if (urlRecord === "failure") {
if (urlRecord === null) {
return attributeValue;
}
return whatwgURL.serializeURL(urlRecord);
};

function isValidAbsoluteURL(str) {
return whatwgURL.parseURL(str) !== "failure";
return whatwgURL.parseURL(str) !== null;
}

exports.isValidTargetOrigin = function (str) {
Expand Down
4 changes: 2 additions & 2 deletions lib/old-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ exports.changeURL = function (window, urlString) {

const url = whatwgURL.parseURL(urlString);

if (url === "failure") {
if (url === null) {
throw new TypeError(`Could not parse "${urlString}" as a URL`);
}

doc._URL = url;
doc._origin = whatwgURL.serializeURLToUnicodeOrigin(doc._URL);
doc._origin = whatwgURL.serializeURLOrigin(doc._URL);
};

// Proxy to features module
Expand Down
18 changes: 8 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"tough-cookie": "^2.3.2",
"webidl-conversions": "^4.0.0",
"whatwg-encoding": "^1.0.1",
"whatwg-url": "^4.3.0",
"whatwg-url": "^6.1.0",
"xml-name-validator": "^2.0.1"
},
"devDependencies": {
Expand Down

0 comments on commit b51255b

Please sign in to comment.