Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Web storage - localStorage, sessionStorage, StorageEvent #2076

Merged
merged 2 commits into from Jun 18, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 47 additions & 0 deletions lib/jsdom/browser/Window.js
Expand Up @@ -23,6 +23,7 @@ const External = require("../living/generated/External");
const Navigator = require("../living/generated/Navigator");
const Performance = require("../living/generated/Performance");
const Screen = require("../living/generated/Screen");
const Storage = require("../living/generated/Storage");
const createAbortController = require("../living/generated/AbortController").createInterface;
const createAbortSignal = require("../living/generated/AbortSignal").createInterface;
const reportException = require("../living/helpers/runtime-script-errors");
Expand Down Expand Up @@ -142,6 +143,38 @@ function Window(options) {

this._pretendToBeVisual = options.pretendToBeVisual;

// Some properties (such as localStorage and sessionStorage) share data
// between windows in the same origin. This object is intended
// to contain such data.
if (options.commonForOrigin && options.commonForOrigin[this._document.origin]) {
this._commonForOrigin = options.commonForOrigin;
} else {
this._commonForOrigin = {
[this._document.origin]: {
localStorageArea: new Map(),
sessionStorageArea: new Map(),
windowsInSameOrigin: [this]
}
};
}

this._currentOriginData = this._commonForOrigin[this._document.origin];

///// WEB STORAGE

this._localStorage = Storage.create([], {
associatedWindow: this,
storageArea: this._currentOriginData.localStorageArea,
type: "localStorage",
url: this._document.documentURI
});
this._sessionStorage = Storage.create([], {
associatedWindow: this,
storageArea: this._currentOriginData.sessionStorageArea,
type: "sessionStorage",
url: this._document.documentURI
});

///// GETTERS

const locationbar = BarProp.create();
Expand Down Expand Up @@ -215,6 +248,20 @@ function Window(options) {
},
get screen() {
return screen;
},
get localStorage() {
if (this._document.origin === "null") {
throw new DOMException("localStorage is not available for opaque origins", "SecurityError");
}

return this._localStorage;
},
get sessionStorage() {
if (this._document.origin === "null") {
throw new DOMException("sessionStorage is not available for opaque origins", "SecurityError");
}

return this._sessionStorage;
}
});

Expand Down
26 changes: 26 additions & 0 deletions lib/jsdom/living/events/StorageEvent-impl.js
@@ -0,0 +1,26 @@
"use strict";

const EventImpl = require("./Event-impl").implementation;

const StorageEventInit = require("../generated/StorageEventInit");

// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
class StorageEventImpl extends EventImpl {
initStorageEvent(type, bubbles, cancelable, key, oldValue, newValue, url, storageArea) {
if (this._dispatchFlag) {
return;
}

this.initEvent(type, bubbles, cancelable);
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
this.url = url;
this.storageArea = storageArea;
}
}
StorageEventImpl.defaultInit = StorageEventInit.convert(undefined);

module.exports = {
implementation: StorageEventImpl
};
17 changes: 17 additions & 0 deletions lib/jsdom/living/events/StorageEvent.webidl
@@ -0,0 +1,17 @@
[Exposed=Window,
Constructor(DOMString type, optional StorageEventInit eventInitDict)]
interface StorageEvent : Event {
readonly attribute DOMString? key;
readonly attribute DOMString? oldValue;
readonly attribute DOMString? newValue;
readonly attribute USVString url;
readonly attribute Storage? storageArea;
};

dictionary StorageEventInit : EventInit {
DOMString? key = null;
DOMString? oldValue = null;
DOMString? newValue = null;
USVString url = "";
Storage? storageArea = null;
};
3 changes: 3 additions & 0 deletions lib/jsdom/living/index.js
Expand Up @@ -38,6 +38,7 @@ exports.MouseEvent = require("./generated/MouseEvent").interface;
exports.KeyboardEvent = require("./generated/KeyboardEvent").interface;
exports.TouchEvent = require("./generated/TouchEvent").interface;
exports.ProgressEvent = require("./generated/ProgressEvent").interface;
exports.StorageEvent = require("./generated/StorageEvent").interface;
exports.CompositionEvent = require("./generated/CompositionEvent").interface;
exports.WheelEvent = require("./generated/WheelEvent").interface;
exports.EventTarget = require("./generated/EventTarget").interface;
Expand All @@ -62,6 +63,8 @@ exports.XMLHttpRequestUpload = require("./generated/XMLHttpRequestUpload").inter
exports.NodeIterator = require("./generated/NodeIterator").interface;
exports.TreeWalker = require("./generated/TreeWalker").interface;

exports.Storage = require("./generated/Storage").interface;

require("./register-elements")(exports);

// These need to be cleaned up...
Expand Down
7 changes: 6 additions & 1 deletion lib/jsdom/living/nodes/HTMLFrameElement-impl.js
Expand Up @@ -45,7 +45,8 @@ function loadFrame(frame) {
agentOptions: parentDoc._agentOptions,
strictSSL: parentDoc._strictSSL,
proxy: parentDoc._proxy,
runScripts: parentDoc._defaultView._runScripts
runScripts: parentDoc._defaultView._runScripts,
commonForOrigin: parentDoc._defaultView._commonForOrigin
});
const contentDoc = frame._contentDocument = idlUtils.implForWrapper(wnd._document);
applyDocumentFeatures(contentDoc, parentDoc._implementation._features);
Expand All @@ -57,6 +58,10 @@ function loadFrame(frame) {
contentWindow._frameElement = frame;
contentWindow._virtualConsole = parent._virtualConsole;

if (parentDoc.origin === contentDoc.origin) {
contentWindow._currentOriginData.windowsInSameOrigin.push(contentWindow);
}

// Handle about:blank with a simulated load of an empty document.
if (serializedURL === "about:blank") {
// Cannot be done inside the enqueued callback; the documentElement etc. need to be immediately available.
Expand Down
98 changes: 98 additions & 0 deletions lib/jsdom/living/webstorage/Storage-impl.js
@@ -0,0 +1,98 @@
"use strict";

const DOMException = require("domexception");
const StorageEvent = require("../generated/StorageEvent");
const idlUtils = require("../generated/utils");

// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
class StorageImpl {
constructor(args, { associatedWindow, storageArea, url, type }) {
this._associatedWindow = associatedWindow;
this._items = storageArea;
this._url = url;
this._type = type;

// The spec suggests a default storage quota of 5 MB
this._quota = 5000;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zirro
As I understand, 5000 would suggest that the default storage quota is 5 KB... shouldn't that be 5_000_000 bytes?

}

_dispatchStorageEvent(key, oldValue, newValue) {
return this._associatedWindow._currentOriginData.windowsInSameOrigin
.filter(target => target !== this._associatedWindow)
.forEach(target => target.dispatchEvent(StorageEvent.create([
"storage",
{
bubbles: false,
cancelable: false,
key,
oldValue,
newValue,
url: this._url,
storageArea: target["_" + this._type]
}
])));
}

get length() {
return this._items.size;
}

key(n) {
if (n >= this._items.size) {
return null;
}
return [...this._items.keys()][n];
}

getItem(key) {
if (this._items.has(key)) {
return this._items.get(key);
}
return null;
}

setItem(key, value) {
const oldValue = this._items.get(key) || null;

if (oldValue === value) {
return;
}

// Concatenate all keys and values to measure their size against the quota
let itemsConcat = key + value;
this._items.forEach((v, k) => {
itemsConcat += v + k;
});
if (Buffer.byteLength(itemsConcat) > this._quota) {
throw new DOMException(`The ${this._quota} byte storage quota has been exceeded.`, "QuotaExceededError");
}

setTimeout(this._dispatchStorageEvent.bind(this), 0, key, oldValue, value);

this._items.set(key, value);
}

removeItem(key) {
if (this._items.has(key)) {
setTimeout(this._dispatchStorageEvent.bind(this), 0, key, this._items.get(key), null);

this._items.delete(key);
}
}

clear() {
if (this._items.size > 0) {
setTimeout(this._dispatchStorageEvent.bind(this), 0, null, null, null);

this._items.clear();
}
}

get [idlUtils.supportedPropertyNames]() {
return this._items.keys();
}
}

module.exports = {
implementation: StorageImpl
};
9 changes: 9 additions & 0 deletions lib/jsdom/living/webstorage/Storage.webidl
@@ -0,0 +1,9 @@
[Exposed=Window]
interface Storage {
readonly attribute unsigned long length;
DOMString? key(unsigned long index);
[WebIDL2JSValueAsUnsupported=null] getter DOMString? getItem(DOMString key);
setter void setItem(DOMString key, DOMString value);
deleter void removeItem(DOMString key);
void clear();
};
2 changes: 1 addition & 1 deletion scripts/webidl/convert.js
Expand Up @@ -32,7 +32,7 @@ addDir("../../lib/jsdom/living/aborting");
addDir("../../lib/jsdom/living/websockets");
addDir("../../lib/jsdom/living/hr-time");
addDir("../../lib/jsdom/living/constraint-validation");

addDir("../../lib/jsdom/living/webstorage");

const outputDir = path.resolve(__dirname, "../../lib/jsdom/living/generated/");

Expand Down
6 changes: 3 additions & 3 deletions test/web-platform-tests/run-wpts.js
Expand Up @@ -14,7 +14,7 @@ const validReasons = new Set([
"mutates-globals",
"needs-await",
"needs-node8",
"fails-node10",
"needs-node10",
"timeout-node6" // For tests that timeout in Node.js v6, but pass in later versions
]);

Expand All @@ -26,7 +26,7 @@ try {
}

const hasNode8 = Number(process.versions.node.split(".")[0]) >= 8;
const isNode10 = Number(process.versions.node.split(".")[0]) === 10;
const hasNode10 = Number(process.versions.node.split(".")[0]) >= 10;

const manifestFilename = path.resolve(__dirname, "wpt-manifest.json");
const manifest = readManifest(manifestFilename);
Expand Down Expand Up @@ -65,7 +65,7 @@ describe("web-platform-tests", () => {
const expectFail = (reason === "fail") ||
(reason === "needs-await" && !supportsAwait) ||
(reason === "needs-node8" && !hasNode8) ||
(reason === "fails-node10" && isNode10);
(reason === "needs-node10" && !hasNode10);

if (matchingPattern && shouldSkip) {
specify.skip(`[${reason}] ${testFile}`);
Expand Down
16 changes: 13 additions & 3 deletions test/web-platform-tests/to-run.yaml
Expand Up @@ -9,7 +9,6 @@ blob/Blob-constructor.html: [fail, "- Blob is not a function
- HTMLSelectElement does not have indexed properties
- MessageChannel not implemented
- element attributes does not have indexed properties"]
blob/Blob-slice.html: [fails-node10, instanceof ArrayBuffer is failing, https://github.com/nodejs/node/issues/20978]
file/File-constructor-endings.html: [needs-await]
file/send-file-form*: [fail, DataTransfer not implemented]
filelist-section/filelist.html: [fail, function is not instanceof Function]
Expand Down Expand Up @@ -338,8 +337,8 @@ browsing-context.html: [fail, Unknown]
nested-browsing-contexts/frameElement.html: [timeout, Unknown]
nested-browsing-contexts/window-parent-null.html: [fail, Unknown]
nested-browsing-contexts/window-top-null.html: [fail, Unknown]
noreferrer-null-opener.html: [timeout, Needs localStorage]
noreferrer-window-name.html: [timeout, Needs localStorage]
noreferrer-null-opener.html: [timeout, Unknown]
noreferrer-window-name.html: [timeout, Depends on URL.createObjectURL]
targeting-cross-origin-nested-browsing-contexts.html: [timeout, Unknown]

---
Expand Down Expand Up @@ -838,6 +837,17 @@ unload-a-document/*: [timeout, Requires window.open]

---

DIR: webstorage

idlharness.html: [fail, Depends on fetch]
storage_enumerate.html: [needs-node8, Uses Object.values()]
storage_local_window_open.html: [timeout, Depends on window.open()]
storage_session_window_noopener.html: [fail, Depends on BroadcastChannel]
storage_session_window_open.html: [timeout, Depends on window.open()]
storage_string_conversion.html: [needs-node10, function.toString() does not use correct formatting in earlier versions, https://github.com/nodejs/node/issues/20459]

---

DIR: xhr

abort-after-stop.htm: [fail, https://github.com/w3c/web-platform-tests/issues/6942]
Expand Down