Skip to content

Commit

Permalink
Add the storageQuota option
Browse files Browse the repository at this point in the history
Also fixes the default quota problem noticed in 3afbc0f#r29395675.
  • Loading branch information
Zirro authored and domenic committed Jul 26, 2018
1 parent b4db242 commit 23d67eb
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 12 deletions.
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -47,7 +47,8 @@ const dom = new JSDOM(``, {
referrer: "https://example.com/",
contentType: "text/html",
userAgent: "Mellblomenator/9000",
includeNodeLocations: true
includeNodeLocations: true,
storageQuota: 10000000
});
```

Expand All @@ -56,6 +57,7 @@ const dom = new JSDOM(``, {
- `contentType` affects the value read from `document.contentType`, and how the document is parsed: as HTML or as XML. Values that are not `"text/html"` or an [XML mime type](https://html.spec.whatwg.org/multipage/infrastructure.html#xml-mime-type) will throw. It defaults to `"text/html"`.
- `userAgent` affects the value read from `navigator.userAgent`, as well as the `User-Agent` header sent while fetching subresources. It defaults to <code>\`Mozilla/5.0 (${process.platform}) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/${jsdomVersion}\`</code>.
- `includeNodeLocations` preserves the location info produced by the HTML parser, allowing you to retrieve it with the `nodeLocation()` method (described below). It also ensures that line numbers reported in exception stack traces for code running inside `<script>` elements are correct. It defaults to `false` to give the best performance, and cannot be used with an XML content type since our XML parser does not support location info.
- `storageQuota` is the maximum size in bytes for the separate storage areas used by `localStorage` and `sessionStorage`. Attempts to store data larger than this limit will cause a `DOMException` to be thrown. By default, it is set to five megabytes per origin as suggested by the HTML specification.

Note that both `url` and `referrer` are canonicalized before they're used, so e.g. if you pass in `"https:example.com"`, jsdom will interpret that as if you had given `"https://example.com/"`. If you pass an unparseable URL, the call will throw. (URLs are parsed and serialized according to the [URL Standard](http://url.spec.whatwg.org/).)

Expand Down
5 changes: 5 additions & 0 deletions lib/api.js
Expand Up @@ -241,6 +241,7 @@ function transformOptions(options, encoding) {
runScripts: undefined,
encoding,
pretendToBeVisual: false,
storageQuota: 5000000,

// Defaults filled in later
virtualConsole: undefined,
Expand Down Expand Up @@ -314,6 +315,10 @@ function transformOptions(options, encoding) {
transformed.windowOptions.pretendToBeVisual = Boolean(options.pretendToBeVisual);
}

if (options.storageQuota !== undefined) {
transformed.windowOptions.storageQuota = Number(options.storageQuota);
}

// concurrentNodeIterators??

return transformed;
Expand Down
7 changes: 5 additions & 2 deletions lib/jsdom/browser/Window.js
Expand Up @@ -142,6 +142,7 @@ function Window(options) {
this._length = 0;

this._pretendToBeVisual = options.pretendToBeVisual;
this._storageQuota = options.storageQuota;

// Some properties (such as localStorage and sessionStorage) share data
// between windows in the same origin. This object is intended
Expand All @@ -166,13 +167,15 @@ function Window(options) {
associatedWindow: this,
storageArea: this._currentOriginData.localStorageArea,
type: "localStorage",
url: this._document.documentURI
url: this._document.documentURI,
storageQuota: this._storageQuota
});
this._sessionStorage = Storage.create([], {
associatedWindow: this,
storageArea: this._currentOriginData.sessionStorageArea,
type: "sessionStorage",
url: this._document.documentURI
url: this._document.documentURI,
storageQuota: this._storageQuota
});

///// GETTERS
Expand Down
15 changes: 8 additions & 7 deletions lib/jsdom/living/webstorage/Storage-impl.js
Expand Up @@ -6,14 +6,12 @@ const idlUtils = require("../generated/utils");

// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
class StorageImpl {
constructor(args, { associatedWindow, storageArea, url, type }) {
constructor(args, { associatedWindow, storageArea, url, type, storageQuota }) {
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;
this._quota = storageQuota;
}

_dispatchStorageEvent(key, oldValue, newValue) {
Expand Down Expand Up @@ -60,9 +58,12 @@ class StorageImpl {

// Concatenate all keys and values to measure their size against the quota
let itemsConcat = key + value;
this._items.forEach((v, k) => {
itemsConcat += v + k;
});
for (const [curKey, curValue] of this._items) {
// If the key already exists, skip it as it will be set to the new value instead
if (key !== curKey) {
itemsConcat += curKey + curValue;
}
}
if (Buffer.byteLength(itemsConcat) > this._quota) {
throw new DOMException(`The ${this._quota} byte storage quota has been exceeded.`, "QuotaExceededError");
}
Expand Down
5 changes: 4 additions & 1 deletion lib/old-api.js
Expand Up @@ -126,6 +126,8 @@ exports.jsdom = function (html, options) {
options.pretendToBeVisual = false;
}

options.storageQuota = options.storageQuota || 5000000;

// List options explicitly to be clear which are passed through
const window = new Window({
parsingMode: options.parsingMode,
Expand All @@ -149,7 +151,8 @@ exports.jsdom = function (html, options) {
proxy: options.proxy,
userAgent: options.userAgent,
runScripts: options.runScripts,
pretendToBeVisual: options.pretendToBeVisual
pretendToBeVisual: options.pretendToBeVisual,
storageQuota: options.storageQuota
});

const documentImpl = idlUtils.implForWrapper(window.document);
Expand Down
70 changes: 70 additions & 0 deletions test/api/options.js
Expand Up @@ -289,4 +289,74 @@ describe("API: constructor options", () => {
});
});
});

describe("storageQuota", () => {
describe("not set", () => {
it("should be 5000000 bytes by default", () => {
const { localStorage, sessionStorage } = (new JSDOM(``, { url: "https://example.com" })).window;
const dataWithinQuota = "0".repeat(4000000);

localStorage.setItem("foo", dataWithinQuota);
sessionStorage.setItem("bar", dataWithinQuota);

assert.strictEqual(localStorage.foo, dataWithinQuota);
assert.strictEqual(sessionStorage.bar, dataWithinQuota);

const dataExceedingQuota = "0".repeat(6000000);

assert.throws(() => localStorage.setItem("foo", dataExceedingQuota));
assert.throws(() => sessionStorage.setItem("bar", dataExceedingQuota));
});
});

describe("set to 10000 bytes", () => {
it("should only allow setting data within the custom quota", () => {
const { localStorage, sessionStorage } = (new JSDOM(``, {
url: "https://example.com",
storageQuota: 10000
})).window;
const dataWithinQuota = "0".repeat(5);

localStorage.setItem("foo", dataWithinQuota);
sessionStorage.setItem("bar", dataWithinQuota);

assert.strictEqual(localStorage.foo, dataWithinQuota);
assert.strictEqual(sessionStorage.bar, dataWithinQuota);

const dataJustWithinQuota = "0".repeat(9995);

localStorage.foo = dataJustWithinQuota;
sessionStorage.bar = dataJustWithinQuota;

assert.strictEqual(localStorage.foo, dataJustWithinQuota);
assert.strictEqual(sessionStorage.bar, dataJustWithinQuota);

const dataExceedingQuota = "0".repeat(15000);

assert.throws(() => localStorage.setItem("foo", dataExceedingQuota));
assert.throws(() => sessionStorage.setItem("bar", dataExceedingQuota));
});
});

describe("set to 10000000 bytes", () => {
it("should only allow setting data within the custom quota", () => {
const { localStorage, sessionStorage } = (new JSDOM(``, {
url: "https://example.com",
storageQuota: 10000000
})).window;
const dataWithinQuota = "0".repeat(8000000);

localStorage.someKey = dataWithinQuota;
sessionStorage.someKey = dataWithinQuota;

assert.strictEqual(localStorage.someKey, dataWithinQuota);
assert.strictEqual(sessionStorage.someKey, dataWithinQuota);

const dataExceedingQuota = "0".repeat(11000000);

assert.throws(() => localStorage.setItem("foo", dataExceedingQuota));
assert.throws(() => sessionStorage.setItem("bar", dataExceedingQuota));
});
});
});
});
3 changes: 2 additions & 1 deletion test/web-platform-tests/run-single-wpt.js
Expand Up @@ -83,7 +83,8 @@ function createJSDOM(urlPrefix, testPath, expectFail) {
doneErrors.push(error);
}
},
pretendToBeVisual: true
pretendToBeVisual: true,
storageQuota: 100000 // Filling the default quota takes about a minute between two WPTs
});
});

Expand Down

0 comments on commit 23d67eb

Please sign in to comment.