summaryrefslogtreecommitdiffstats
path: root/dom/manifest
diff options
context:
space:
mode:
Diffstat (limited to 'dom/manifest')
-rw-r--r--dom/manifest/ImageObjectProcessor.jsm153
-rw-r--r--dom/manifest/ManifestFinder.jsm68
-rw-r--r--dom/manifest/ManifestObtainer.jsm160
-rw-r--r--dom/manifest/ManifestProcessor.jsm273
-rw-r--r--dom/manifest/ValueExtractor.jsm65
-rw-r--r--dom/manifest/moz.build16
-rw-r--r--dom/manifest/test/browser.ini9
-rw-r--r--dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js84
-rw-r--r--dom/manifest/test/browser_ManifestObtainer_obtain.js181
-rw-r--r--dom/manifest/test/browser_fire_appinstalled_event.js49
-rw-r--r--dom/manifest/test/common.js22
-rw-r--r--dom/manifest/test/file_reg_appinstalled_event.html15
-rw-r--r--dom/manifest/test/file_testserver.sjs54
-rw-r--r--dom/manifest/test/manifestLoader.html13
-rw-r--r--dom/manifest/test/mochitest.ini23
-rw-r--r--dom/manifest/test/resource.sjs85
-rw-r--r--dom/manifest/test/test_ImageObjectProcessor_sizes.html95
-rw-r--r--dom/manifest/test/test_ImageObjectProcessor_src.html106
-rw-r--r--dom/manifest/test/test_ImageObjectProcessor_type.html57
-rw-r--r--dom/manifest/test/test_ManifestProcessor_JSON.html34
-rw-r--r--dom/manifest/test/test_ManifestProcessor_background_color.html118
-rw-r--r--dom/manifest/test/test_ManifestProcessor_dir.html57
-rw-r--r--dom/manifest/test/test_ManifestProcessor_display.html78
-rw-r--r--dom/manifest/test/test_ManifestProcessor_icons.html30
-rw-r--r--dom/manifest/test/test_ManifestProcessor_lang.html112
-rw-r--r--dom/manifest/test/test_ManifestProcessor_name_and_short_name.html79
-rw-r--r--dom/manifest/test/test_ManifestProcessor_orientation.html86
-rw-r--r--dom/manifest/test/test_ManifestProcessor_scope.html89
-rw-r--r--dom/manifest/test/test_ManifestProcessor_start_url.html59
-rw-r--r--dom/manifest/test/test_ManifestProcessor_theme_color.html118
-rw-r--r--dom/manifest/test/test_ManifestProcessor_warnings.html90
-rw-r--r--dom/manifest/test/test_window_onappinstalled_event.html98
32 files changed, 2576 insertions, 0 deletions
diff --git a/dom/manifest/ImageObjectProcessor.jsm b/dom/manifest/ImageObjectProcessor.jsm
new file mode 100644
index 000000000..7ef5fe811
--- /dev/null
+++ b/dom/manifest/ImageObjectProcessor.jsm
@@ -0,0 +1,153 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+/*
+ * ImageObjectProcessor
+ * Implementation of Image Object processing algorithms from:
+ * http://www.w3.org/TR/appmanifest/#image-object-and-its-members
+ *
+ * This is intended to be used in conjunction with ManifestProcessor.jsm
+ *
+ * Creates an object to process Image Objects as defined by the
+ * W3C specification. This is used to process things like the
+ * icon member and the splash_screen member.
+ *
+ * Usage:
+ *
+ * .process(aManifest, aBaseURL, aMemberName);
+ *
+ */
+/*exported EXPORTED_SYMBOLS*/
+/*globals Components */
+'use strict';
+const {
+ utils: Cu,
+ interfaces: Ci,
+ classes: Cc
+} = Components;
+
+Cu.importGlobalProperties(['URL']);
+const netutil = Cc['@mozilla.org/network/util;1']
+ .getService(Ci.nsINetUtil);
+
+function ImageObjectProcessor(aConsole, aExtractor) {
+ this.console = aConsole;
+ this.extractor = aExtractor;
+}
+
+// Static getters
+Object.defineProperties(ImageObjectProcessor, {
+ 'decimals': {
+ get: function() {
+ return /^\d+$/;
+ }
+ },
+ 'anyRegEx': {
+ get: function() {
+ return new RegExp('any', 'i');
+ }
+ }
+});
+
+ImageObjectProcessor.prototype.process = function(
+ aManifest, aBaseURL, aMemberName
+) {
+ const spec = {
+ objectName: 'manifest',
+ object: aManifest,
+ property: aMemberName,
+ expectedType: 'array',
+ trim: false
+ };
+ const extractor = this.extractor;
+ const images = [];
+ const value = extractor.extractValue(spec);
+ if (Array.isArray(value)) {
+ // Filter out images whose "src" is not useful.
+ value.filter(item => !!processSrcMember(item, aBaseURL))
+ .map(toImageObject)
+ .forEach(image => images.push(image));
+ }
+ return images;
+
+ function toImageObject(aImageSpec) {
+ return {
+ 'src': processSrcMember(aImageSpec, aBaseURL),
+ 'type': processTypeMember(aImageSpec),
+ 'sizes': processSizesMember(aImageSpec),
+ };
+ }
+
+ function processTypeMember(aImage) {
+ const charset = {};
+ const hadCharset = {};
+ const spec = {
+ objectName: 'image',
+ object: aImage,
+ property: 'type',
+ expectedType: 'string',
+ trim: true
+ };
+ let value = extractor.extractValue(spec);
+ if (value) {
+ value = netutil.parseRequestContentType(value, charset, hadCharset);
+ }
+ return value || undefined;
+ }
+
+ function processSrcMember(aImage, aBaseURL) {
+ const spec = {
+ objectName: 'image',
+ object: aImage,
+ property: 'src',
+ expectedType: 'string',
+ trim: false
+ };
+ const value = extractor.extractValue(spec);
+ let url;
+ if (value && value.length) {
+ try {
+ url = new URL(value, aBaseURL).href;
+ } catch (e) {}
+ }
+ return url;
+ }
+
+ function processSizesMember(aImage) {
+ const sizes = new Set();
+ const spec = {
+ objectName: 'image',
+ object: aImage,
+ property: 'sizes',
+ expectedType: 'string',
+ trim: true
+ };
+ const value = extractor.extractValue(spec);
+ if (value) {
+ // Split on whitespace and filter out invalid values.
+ value.split(/\s+/)
+ .filter(isValidSizeValue)
+ .reduce((collector, size) => collector.add(size), sizes);
+ }
+ return (sizes.size) ? Array.from(sizes).join(" ") : undefined;
+ // Implementation of HTML's link@size attribute checker.
+ function isValidSizeValue(aSize) {
+ const size = aSize.toLowerCase();
+ if (ImageObjectProcessor.anyRegEx.test(aSize)) {
+ return true;
+ }
+ if (!size.includes('x') || size.indexOf('x') !== size.lastIndexOf('x')) {
+ return false;
+ }
+ // Split left of x for width, after x for height.
+ const widthAndHeight = size.split('x');
+ const w = widthAndHeight.shift();
+ const h = widthAndHeight.join('x');
+ const validStarts = !w.startsWith('0') && !h.startsWith('0');
+ const validDecimals = ImageObjectProcessor.decimals.test(w + h);
+ return (validStarts && validDecimals);
+ }
+ }
+};
+this.ImageObjectProcessor = ImageObjectProcessor; // jshint ignore:line
+this.EXPORTED_SYMBOLS = ['ImageObjectProcessor']; // jshint ignore:line
diff --git a/dom/manifest/ManifestFinder.jsm b/dom/manifest/ManifestFinder.jsm
new file mode 100644
index 000000000..1e9a9fb15
--- /dev/null
+++ b/dom/manifest/ManifestFinder.jsm
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+/* globals Components, Task, PromiseMessage */
+"use strict";
+const {
+ utils: Cu
+} = Components;
+Cu.import("resource://gre/modules/PromiseMessage.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.ManifestFinder = {// jshint ignore:line
+ /**
+ * Check from content process if DOM Window has a conforming
+ * manifest link relationship.
+ * @param aContent DOM Window to check.
+ * @return {Promise<Boolean>}
+ */
+ contentHasManifestLink(aContent) {
+ if (!aContent || isXULBrowser(aContent)) {
+ throw new TypeError("Invalid input.");
+ }
+ return checkForManifest(aContent);
+ },
+
+ /**
+ * Check from a XUL browser (parent process) if it's content document has a
+ * manifest link relationship.
+ * @param aBrowser The XUL browser to check.
+ * @return {Promise}
+ */
+ browserHasManifestLink: Task.async(
+ function* (aBrowser) {
+ if (!isXULBrowser(aBrowser)) {
+ throw new TypeError("Invalid input.");
+ }
+ const msgKey = "DOM:WebManifest:hasManifestLink";
+ const mm = aBrowser.messageManager;
+ const reply = yield PromiseMessage.send(mm, msgKey);
+ return reply.data.result;
+ }
+ )
+};
+
+function isXULBrowser(aBrowser) {
+ if (!aBrowser || !aBrowser.namespaceURI || !aBrowser.localName) {
+ return false;
+ }
+ const XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return (aBrowser.namespaceURI === XUL && aBrowser.localName === "browser");
+}
+
+function checkForManifest(aWindow) {
+ // Only top-level browsing contexts are valid.
+ if (!aWindow || aWindow.top !== aWindow) {
+ return false;
+ }
+ const elem = aWindow.document.querySelector("link[rel~='manifest']");
+ // Only if we have an element and a non-empty href attribute.
+ if (!elem || !elem.getAttribute("href")) {
+ return false;
+ }
+ return true;
+}
+
+this.EXPORTED_SYMBOLS = [// jshint ignore:line
+ "ManifestFinder"
+];
diff --git a/dom/manifest/ManifestObtainer.jsm b/dom/manifest/ManifestObtainer.jsm
new file mode 100644
index 000000000..88cba0d91
--- /dev/null
+++ b/dom/manifest/ManifestObtainer.jsm
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+ /*
+ * ManifestObtainer is an implementation of:
+ * http://w3c.github.io/manifest/#obtaining
+ *
+ * Exposes 2 public method:
+ *
+ * .contentObtainManifest(aContent) - used in content process
+ * .browserObtainManifest(aBrowser) - used in browser/parent process
+ *
+ * both return a promise. If successful, you get back a manifest object.
+ *
+ * Import it with URL:
+ * 'chrome://global/content/manifestMessages.js'
+ *
+ * e10s IPC message from this components are handled by:
+ * dom/ipc/manifestMessages.js
+ *
+ * Which is injected into every browser instance via browser.js.
+ *
+ * exported ManifestObtainer
+ */
+/*globals Components, Task, PromiseMessage, XPCOMUtils, ManifestProcessor, BrowserUtils*/
+"use strict";
+const {
+ utils: Cu,
+ classes: Cc,
+ interfaces: Ci
+} = Components;
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/PromiseMessage.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/ManifestProcessor.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", // jshint ignore:line
+ "resource://gre/modules/BrowserUtils.jsm");
+
+this.ManifestObtainer = { // jshint ignore:line
+ /**
+ * Public interface for obtaining a web manifest from a XUL browser, to use
+ * on the parent process.
+ * @param {XULBrowser} The browser to check for the manifest.
+ * @return {Promise<Object>} The processed manifest.
+ */
+ browserObtainManifest: Task.async(function* (aBrowser) {
+ const msgKey = "DOM:ManifestObtainer:Obtain";
+ if (!isXULBrowser(aBrowser)) {
+ throw new TypeError("Invalid input. Expected XUL browser.");
+ }
+ const mm = aBrowser.messageManager;
+ const {data: {success, result}} = yield PromiseMessage.send(mm, msgKey);
+ if (!success) {
+ const error = toError(result);
+ throw error;
+ }
+ return result;
+ }),
+ /**
+ * Public interface for obtaining a web manifest from a XUL browser.
+ * @param {Window} The content Window from which to extract the manifest.
+ * @return {Promise<Object>} The processed manifest.
+ */
+ contentObtainManifest: Task.async(function* (aContent) {
+ if (!aContent || isXULBrowser(aContent)) {
+ throw new TypeError("Invalid input. Expected a DOM Window.");
+ }
+ let manifest;
+ try {
+ manifest = yield fetchManifest(aContent);
+ } catch (err) {
+ throw err;
+ }
+ return manifest;
+ }
+)};
+
+function toError(aErrorClone) {
+ let error;
+ switch (aErrorClone.name) {
+ case "TypeError":
+ error = new TypeError();
+ break;
+ default:
+ error = new Error();
+ }
+ Object.getOwnPropertyNames(aErrorClone)
+ .forEach(name => error[name] = aErrorClone[name]);
+ return error;
+}
+
+function isXULBrowser(aBrowser) {
+ if (!aBrowser || !aBrowser.namespaceURI || !aBrowser.localName) {
+ return false;
+ }
+ const XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return (aBrowser.namespaceURI === XUL && aBrowser.localName === "browser");
+}
+
+/**
+ * Asynchronously processes the result of response after having fetched
+ * a manifest.
+ * @param {Response} aResp Response from fetch().
+ * @param {Window} aContentWindow The content window.
+ * @return {Promise<Object>} The processed manifest.
+ */
+const processResponse = Task.async(function* (aResp, aContentWindow) {
+ const badStatus = aResp.status < 200 || aResp.status >= 300;
+ if (aResp.type === "error" || badStatus) {
+ const msg =
+ `Fetch error: ${aResp.status} - ${aResp.statusText} at ${aResp.url}`;
+ throw new Error(msg);
+ }
+ const text = yield aResp.text();
+ const args = {
+ jsonText: text,
+ manifestURL: aResp.url,
+ docURL: aContentWindow.location.href
+ };
+ const manifest = ManifestProcessor.process(args);
+ return manifest;
+});
+
+/**
+ * Asynchronously fetches a web manifest.
+ * @param {Window} a The content Window from where to extract the manifest.
+ * @return {Promise<Object>}
+ */
+const fetchManifest = Task.async(function* (aWindow) {
+ if (!aWindow || aWindow.top !== aWindow) {
+ let msg = "Window must be a top-level browsing context.";
+ throw new Error(msg);
+ }
+ const elem = aWindow.document.querySelector("link[rel~='manifest']");
+ if (!elem || !elem.getAttribute("href")) {
+ let msg = `No manifest to fetch at ${aWindow.location}`;
+ throw new Error(msg);
+ }
+ // Throws on malformed URLs
+ const manifestURL = new aWindow.URL(elem.href, elem.baseURI);
+ const reqInit = {
+ mode: "cors"
+ };
+ if (elem.crossOrigin === "use-credentials") {
+ reqInit.credentials = "include";
+ }
+ const request = new aWindow.Request(manifestURL, reqInit);
+ request.overrideContentPolicyType(Ci.nsIContentPolicy.TYPE_WEB_MANIFEST);
+ let response;
+ try {
+ response = yield aWindow.fetch(request);
+ } catch (err) {
+ throw err;
+ }
+ const manifest = yield processResponse(response, aWindow);
+ return manifest;
+});
+
+this.EXPORTED_SYMBOLS = ["ManifestObtainer"]; // jshint ignore:line
diff --git a/dom/manifest/ManifestProcessor.jsm b/dom/manifest/ManifestProcessor.jsm
new file mode 100644
index 000000000..c4f837009
--- /dev/null
+++ b/dom/manifest/ManifestProcessor.jsm
@@ -0,0 +1,273 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/*
+ * ManifestProcessor
+ * Implementation of processing algorithms from:
+ * http://www.w3.org/2008/webapps/manifest/
+ *
+ * Creates manifest processor that lets you process a JSON file
+ * or individual parts of a manifest object. A manifest is just a
+ * standard JS object that has been cleaned up.
+ *
+ * .process({jsonText,manifestURL,docURL});
+ *
+ * Depends on ImageObjectProcessor to process things like
+ * icons and splash_screens.
+ *
+ * TODO: The constructor should accept the UA's supported orientations.
+ * TODO: The constructor should accept the UA's supported display modes.
+ * TODO: hook up developer tools to console. (1086997).
+ */
+/*globals Components, ValueExtractor, ImageObjectProcessor, ConsoleAPI*/
+'use strict';
+const {
+ utils: Cu
+} = Components;
+Cu.importGlobalProperties(['URL']);
+const displayModes = new Set(['fullscreen', 'standalone', 'minimal-ui',
+ 'browser'
+]);
+const orientationTypes = new Set(['any', 'natural', 'landscape', 'portrait',
+ 'portrait-primary', 'portrait-secondary', 'landscape-primary',
+ 'landscape-secondary'
+]);
+const textDirections = new Set(['ltr', 'rtl', 'auto']);
+
+Cu.import('resource://gre/modules/Console.jsm');
+Cu.import("resource://gre/modules/Services.jsm");
+// ValueExtractor is used by the various processors to get values
+// from the manifest and to report errors.
+Cu.import('resource://gre/modules/ValueExtractor.jsm');
+// ImageObjectProcessor is used to process things like icons and images
+Cu.import('resource://gre/modules/ImageObjectProcessor.jsm');
+
+this.ManifestProcessor = { // jshint ignore:line
+ get defaultDisplayMode() {
+ return 'browser';
+ },
+ get displayModes() {
+ return displayModes;
+ },
+ get orientationTypes() {
+ return orientationTypes;
+ },
+ get textDirections() {
+ return textDirections;
+ },
+ // process() method processes JSON text into a clean manifest
+ // that conforms with the W3C specification. Takes an object
+ // expecting the following dictionary items:
+ // * jsonText: the JSON string to be processed.
+ // * manifestURL: the URL of the manifest, to resolve URLs.
+ // * docURL: the URL of the owner doc, for security checks
+ process({
+ jsonText,
+ manifestURL: aManifestURL,
+ docURL: aDocURL
+ }) {
+ const domBundle = Services.strings.createBundle("chrome://global/locale/dom/dom.properties");
+
+ const console = new ConsoleAPI({
+ prefix: 'Web Manifest'
+ });
+ const manifestURL = new URL(aManifestURL);
+ const docURL = new URL(aDocURL);
+ let rawManifest = {};
+ try {
+ rawManifest = JSON.parse(jsonText);
+ } catch (e) {}
+ if (typeof rawManifest !== 'object' || rawManifest === null) {
+ console.warn(domBundle.GetStringFromName('ManifestShouldBeObject'));
+ rawManifest = {};
+ }
+ const extractor = new ValueExtractor(console, domBundle);
+ const imgObjProcessor = new ImageObjectProcessor(console, extractor);
+ const processedManifest = {
+ 'dir': processDirMember.call(this),
+ 'lang': processLangMember(),
+ 'start_url': processStartURLMember(),
+ 'display': processDisplayMember.call(this),
+ 'orientation': processOrientationMember.call(this),
+ 'name': processNameMember(),
+ 'icons': imgObjProcessor.process(
+ rawManifest, manifestURL, 'icons'
+ ),
+ 'short_name': processShortNameMember(),
+ 'theme_color': processThemeColorMember(),
+ 'background_color': processBackgroundColorMember(),
+ };
+ processedManifest.scope = processScopeMember();
+ return processedManifest;
+
+ function processDirMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'dir',
+ expectedType: 'string',
+ trim: true,
+ };
+ const value = extractor.extractValue(spec);
+ if (this.textDirections.has(value)) {
+ return value;
+ }
+ return 'auto';
+ }
+
+ function processNameMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'name',
+ expectedType: 'string',
+ trim: true
+ };
+ return extractor.extractValue(spec);
+ }
+
+ function processShortNameMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'short_name',
+ expectedType: 'string',
+ trim: true
+ };
+ return extractor.extractValue(spec);
+ }
+
+ function processOrientationMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'orientation',
+ expectedType: 'string',
+ trim: true
+ };
+ const value = extractor.extractValue(spec);
+ if (value && typeof value === "string" && this.orientationTypes.has(value.toLowerCase())) {
+ return value.toLowerCase();
+ }
+ return undefined;
+ }
+
+ function processDisplayMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'display',
+ expectedType: 'string',
+ trim: true
+ };
+ const value = extractor.extractValue(spec);
+ if (value && typeof value === "string" && displayModes.has(value.toLowerCase())) {
+ return value.toLowerCase();
+ }
+ return this.defaultDisplayMode;
+ }
+
+ function processScopeMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'scope',
+ expectedType: 'string',
+ trim: false
+ };
+ let scopeURL;
+ const startURL = new URL(processedManifest.start_url);
+ const value = extractor.extractValue(spec);
+ if (value === undefined || value === '') {
+ return undefined;
+ }
+ try {
+ scopeURL = new URL(value, manifestURL);
+ } catch (e) {
+ console.warn(domBundle.GetStringFromName('ManifestScopeURLInvalid'));
+ return undefined;
+ }
+ if (scopeURL.origin !== docURL.origin) {
+ console.warn(domBundle.GetStringFromName('ManifestScopeNotSameOrigin'));
+ return undefined;
+ }
+ // If start URL is not within scope of scope URL:
+ let isSameOrigin = startURL && startURL.origin !== scopeURL.origin;
+ if (isSameOrigin || !startURL.pathname.startsWith(scopeURL.pathname)) {
+ console.warn(domBundle.GetStringFromName('ManifestStartURLOutsideScope'));
+ return undefined;
+ }
+ return scopeURL.href;
+ }
+
+ function processStartURLMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'start_url',
+ expectedType: 'string',
+ trim: false
+ };
+ let result = new URL(docURL).href;
+ const value = extractor.extractValue(spec);
+ if (value === undefined || value === '') {
+ return result;
+ }
+ let potentialResult;
+ try {
+ potentialResult = new URL(value, manifestURL);
+ } catch (e) {
+ console.warn(domBundle.GetStringFromName('ManifestStartURLInvalid'))
+ return result;
+ }
+ if (potentialResult.origin !== docURL.origin) {
+ console.warn(domBundle.GetStringFromName('ManifestStartURLShouldBeSameOrigin'));
+ } else {
+ result = potentialResult.href;
+ }
+ return result;
+ }
+
+ function processThemeColorMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'theme_color',
+ expectedType: 'string',
+ trim: true
+ };
+ return extractor.extractColorValue(spec);
+ }
+
+ function processBackgroundColorMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'background_color',
+ expectedType: 'string',
+ trim: true
+ };
+ return extractor.extractColorValue(spec);
+ }
+
+ function processLangMember() {
+ const spec = {
+ objectName: 'manifest',
+ object: rawManifest,
+ property: 'lang',
+ expectedType: 'string', trim: true
+ };
+ let tag = extractor.extractValue(spec);
+ // TODO: Check if tag is structurally valid.
+ // Cannot do this because we don't support Intl API on Android.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=864843
+ // https://github.com/tc39/ecma402/issues/5
+ // TODO: perform canonicalization on the tag.
+ // Can't do this today because there is no direct means to
+ // access canonicalization algorithms through Intl API.
+ // https://github.com/tc39/ecma402/issues/5
+ return tag;
+ }
+ }
+};
+this.EXPORTED_SYMBOLS = ['ManifestProcessor']; // jshint ignore:line
diff --git a/dom/manifest/ValueExtractor.jsm b/dom/manifest/ValueExtractor.jsm
new file mode 100644
index 000000000..f3dd25905
--- /dev/null
+++ b/dom/manifest/ValueExtractor.jsm
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+/*
+ * Helper functions extract values from manifest members
+ * and reports conformance violations.
+ */
+/*globals Components*/
+'use strict';
+const {
+ classes: Cc,
+ interfaces: Ci
+} = Components;
+
+function ValueExtractor(aConsole, aBundle) {
+ this.console = aConsole;
+ this.domBundle = aBundle;
+}
+
+ValueExtractor.prototype = {
+ // This function takes a 'spec' object and destructures
+ // it to extract a value. If the value is of th wrong type, it
+ // warns the developer and returns undefined.
+ // expectType: is the type of a JS primitive (string, number, etc.)
+ // object: is the object from which to extract the value.
+ // objectName: string used to construct the developer warning.
+ // property: the name of the property being extracted.
+ // trim: boolean, if the value should be trimmed (used by string type).
+ extractValue({expectedType, object, objectName, property, trim}) {
+ const value = object[property];
+ const isArray = Array.isArray(value);
+ // We need to special-case "array", as it's not a JS primitive.
+ const type = (isArray) ? 'array' : typeof value;
+ if (type !== expectedType) {
+ if (type !== 'undefined') {
+ this.console.warn(this.domBundle.formatStringFromName("ManifestInvalidType",
+ [objectName, property, expectedType],
+ 3));
+ }
+ return undefined;
+ }
+ // Trim string and returned undefined if the empty string.
+ const shouldTrim = expectedType === 'string' && value && trim;
+ if (shouldTrim) {
+ return value.trim() || undefined;
+ }
+ return value;
+ },
+ extractColorValue(spec) {
+ const value = this.extractValue(spec);
+ const DOMUtils = Cc['@mozilla.org/inspector/dom-utils;1']
+ .getService(Ci.inIDOMUtils);
+ let color;
+ if (DOMUtils.isValidCSSColor(value)) {
+ color = value;
+ } else if (value) {
+ this.console.warn(this.domBundle.formatStringFromName("ManifestInvalidCSSColor",
+ [spec.property, value],
+ 2));
+ }
+ return color;
+ }
+};
+this.ValueExtractor = ValueExtractor; // jshint ignore:line
+this.EXPORTED_SYMBOLS = ['ValueExtractor']; // jshint ignore:line
diff --git a/dom/manifest/moz.build b/dom/manifest/moz.build
new file mode 100644
index 000000000..338794a19
--- /dev/null
+++ b/dom/manifest/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES += [
+ 'ImageObjectProcessor.jsm',
+ 'ManifestFinder.jsm',
+ 'ManifestObtainer.jsm',
+ 'ManifestProcessor.jsm',
+ 'ValueExtractor.jsm',
+]
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
+BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
diff --git a/dom/manifest/test/browser.ini b/dom/manifest/test/browser.ini
new file mode 100644
index 000000000..ad98fe26c
--- /dev/null
+++ b/dom/manifest/test/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ file_reg_appinstalled_event.html
+ file_testserver.sjs
+ manifestLoader.html
+ resource.sjs
+[browser_ManifestFinder_browserHasManifestLink.js]
+[browser_ManifestObtainer_obtain.js]
+[browser_fire_appinstalled_event.js] \ No newline at end of file
diff --git a/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js b/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js
new file mode 100644
index 000000000..5ec663962
--- /dev/null
+++ b/dom/manifest/test/browser_ManifestFinder_browserHasManifestLink.js
@@ -0,0 +1,84 @@
+//Used by JSHint:
+/*global Cu, BrowserTestUtils, ok, add_task, gBrowser */
+"use strict";
+const { ManifestFinder } = Cu.import("resource://gre/modules/ManifestFinder.jsm", {});
+const defaultURL = new URL("http://example.org/browser/dom/manifest/test/resource.sjs");
+defaultURL.searchParams.set("Content-Type", "text/html; charset=utf-8");
+
+const tests = [{
+ body: `
+ <link rel="manifesto" href='${defaultURL}?body={"name":"fail"}'>
+ <link rel="foo bar manifest bar test" href='${defaultURL}?body={"name":"value"}'>
+ <link rel="manifest" href='${defaultURL}?body={"name":"fail"}'>
+ `,
+ run(result) {
+ ok(result, "Document has a web manifest.");
+ },
+}, {
+ body: `
+ <link rel="amanifista" href='${defaultURL}?body={"name":"fail"}'>
+ <link rel="foo bar manifesto bar test" href='${defaultURL}?body={"name":"pass-1"}'>
+ <link rel="manifesto" href='${defaultURL}?body={"name":"fail"}'>`,
+ run(result) {
+ ok(!result, "Document does not have a web manifest.");
+ },
+}, {
+ body: `
+ <link rel="manifest" href="">
+ <link rel="manifest" href='${defaultURL}?body={"name":"fail"}'>`,
+ run(result) {
+ ok(!result, "Manifest link is has empty href.");
+ },
+}, {
+ body: `
+ <link rel="manifest">
+ <link rel="manifest" href='${defaultURL}?body={"name":"fail"}'>`,
+ run(result) {
+ ok(!result, "Manifest link is missing.");
+ },
+}];
+
+function makeTestURL({ body }) {
+ const url = new URL(defaultURL);
+ url.searchParams.set("body", encodeURIComponent(body));
+ return url.href;
+}
+
+/**
+ * Test basic API error conditions
+ */
+add_task(function*() {
+ const expected = "Invalid types should throw a TypeError.";
+ for (let invalidValue of [undefined, null, 1, {}, "test"]) {
+ try {
+ yield ManifestFinder.contentManifestLink(invalidValue);
+ ok(false, expected);
+ } catch (e) {
+ is(e.name, "TypeError", expected);
+ }
+ try {
+ yield ManifestFinder.browserManifestLink(invalidValue);
+ ok(false, expected);
+ } catch (e) {
+ is(e.name, "TypeError", expected);
+ }
+ }
+});
+
+add_task(function*() {
+ const runningTests = tests
+ .map(
+ test => ({
+ gBrowser,
+ test,
+ url: makeTestURL(test),
+ })
+ )
+ .map(
+ tabOptions => BrowserTestUtils.withNewTab(tabOptions, function*(browser) {
+ const result = yield ManifestFinder.browserHasManifestLink(browser);
+ tabOptions.test.run(result);
+ })
+ );
+ yield Promise.all(runningTests);
+});
diff --git a/dom/manifest/test/browser_ManifestObtainer_obtain.js b/dom/manifest/test/browser_ManifestObtainer_obtain.js
new file mode 100644
index 000000000..a2e468905
--- /dev/null
+++ b/dom/manifest/test/browser_ManifestObtainer_obtain.js
@@ -0,0 +1,181 @@
+//Used by JSHint:
+/*global ok, is, Cu, BrowserTestUtils, add_task, gBrowser, makeTestURL, requestLongerTimeout*/
+'use strict';
+const { ManifestObtainer } = Cu.import('resource://gre/modules/ManifestObtainer.jsm', {});
+const remoteURL = 'http://mochi.test:8888/browser/dom/manifest/test/resource.sjs';
+const defaultURL = new URL('http://example.org/browser/dom/manifest/test/resource.sjs');
+defaultURL.searchParams.set('Content-Type', 'text/html; charset=utf-8');
+requestLongerTimeout(4);
+
+const tests = [
+ // Fetch tests.
+ {
+ body: `
+ <link rel="manifesto" href='resource.sjs?body={"name":"fail"}'>
+ <link rel="foo bar manifest bar test" href='resource.sjs?body={"name":"pass-1"}'>
+ <link rel="manifest" href='resource.sjs?body={"name":"fail"}'>`,
+ run(manifest) {
+ is(manifest.name, 'pass-1', 'Manifest is first `link` where @rel contains token manifest.');
+ }
+ }, {
+ body: `
+ <link rel="foo bar manifest bar test" href='resource.sjs?body={"name":"pass-2"}'>
+ <link rel="manifest" href='resource.sjs?body={"name":"fail"}'>
+ <link rel="manifest foo bar test" href='resource.sjs?body={"name":"fail"}'>`,
+ run(manifest) {
+ is(manifest.name, 'pass-2', 'Manifest is first `link` where @rel contains token manifest.');
+ },
+ }, {
+ body: `<link rel="manifest" href='${remoteURL}?body={"name":"pass-3"}'>`,
+ run(err) {
+ is(err.name, 'TypeError', 'By default, manifest cannot load cross-origin.');
+ },
+ },
+ // CORS Tests.
+ {
+ get body() {
+ const body = 'body={"name": "pass-4"}';
+ const CORS =
+ `Access-Control-Allow-Origin=${defaultURL.origin}`;
+ const link =
+ `<link
+ crossorigin=anonymous
+ rel="manifest"
+ href='${remoteURL}?${body}&${CORS}'>`;
+ return link;
+ },
+ run(manifest) {
+ is(manifest.name, 'pass-4', 'CORS enabled, manifest must be fetched.');
+ },
+ }, {
+ get body() {
+ const body = 'body={"name": "fail"}';
+ const CORS = 'Access-Control-Allow-Origin=http://not-here';
+ const link =
+ `<link
+ crossorigin
+ rel="manifest"
+ href='${remoteURL}?${body}&${CORS}'>`;
+ return link;
+ },
+ run(err) {
+ is(err.name, 'TypeError', 'Fetch blocked by CORS - origin does not match.');
+ },
+ }, {
+ body: `<link rel="manifest" href='about:whatever'>`,
+ run(err) {
+ is(err.name, 'TypeError', 'Trying to load from about:whatever is TypeError.');
+ },
+ }, {
+ body: `<link rel="manifest" href='file://manifest'>`,
+ run(err) {
+ is(err.name, 'TypeError', 'Trying to load from file://whatever is a TypeError.');
+ },
+ },
+ //URL parsing tests
+ {
+ body: `<link rel="manifest" href='http://[12.1212.21.21.12.21.12]'>`,
+ run(err) {
+ is(err.name, 'TypeError', 'Trying to load invalid URL is a TypeError.');
+ },
+ },
+];
+
+function makeTestURL({ body }) {
+ const url = new URL(defaultURL);
+ url.searchParams.set('body', encodeURIComponent(body));
+ return url.href;
+}
+
+add_task(function*() {
+ const promises = tests
+ .map(test => ({
+ gBrowser,
+ testRunner: testObtainingManifest(test),
+ url: makeTestURL(test)
+ }))
+ .reduce((collector, tabOpts) => {
+ const promise = BrowserTestUtils.withNewTab(tabOpts, tabOpts.testRunner);
+ collector.push(promise);
+ return collector;
+ }, []);
+
+ const results = yield Promise.all(promises);
+
+ function testObtainingManifest(aTest) {
+ return function*(aBrowser) {
+ try {
+ const manifest = yield ManifestObtainer.browserObtainManifest(aBrowser);
+ aTest.run(manifest);
+ } catch (e) {
+ aTest.run(e);
+ }
+ };
+ }
+});
+
+/*
+ * e10s race condition tests
+ * Open a bunch of tabs and load manifests
+ * in each tab. They should all return pass.
+ */
+add_task(function*() {
+ const defaultPath = '/browser/dom/manifest/test/manifestLoader.html';
+ const tabURLs = [
+ `http://example.com:80${defaultPath}`,
+ `http://example.org:80${defaultPath}`,
+ `http://example.org:8000${defaultPath}`,
+ `http://mochi.test:8888${defaultPath}`,
+ `http://sub1.test1.example.com:80${defaultPath}`,
+ `http://sub1.test1.example.org:80${defaultPath}`,
+ `http://sub1.test1.example.org:8000${defaultPath}`,
+ `http://sub1.test1.mochi.test:8888${defaultPath}`,
+ `http://sub1.test2.example.com:80${defaultPath}`,
+ `http://sub1.test2.example.org:80${defaultPath}`,
+ `http://sub1.test2.example.org:8000${defaultPath}`,
+ `http://sub2.test1.example.com:80${defaultPath}`,
+ `http://sub2.test1.example.org:80${defaultPath}`,
+ `http://sub2.test1.example.org:8000${defaultPath}`,
+ `http://sub2.test2.example.com:80${defaultPath}`,
+ `http://sub2.test2.example.org:80${defaultPath}`,
+ `http://sub2.test2.example.org:8000${defaultPath}`,
+ `http://sub2.xn--lt-uia.mochi.test:8888${defaultPath}`,
+ `http://test1.example.com:80${defaultPath}`,
+ `http://test1.example.org:80${defaultPath}`,
+ `http://test1.example.org:8000${defaultPath}`,
+ `http://test1.mochi.test:8888${defaultPath}`,
+ `http://test2.example.com:80${defaultPath}`,
+ `http://test2.example.org:80${defaultPath}`,
+ `http://test2.example.org:8000${defaultPath}`,
+ `http://test2.mochi.test:8888${defaultPath}`,
+ `http://test:80${defaultPath}`,
+ `http://www.example.com:80${defaultPath}`,
+ ];
+ // Open tabs an collect corresponding browsers
+ let browsers = [
+ for (url of tabURLs) gBrowser.addTab(url).linkedBrowser
+ ];
+ // Once all the pages have loaded, run a bunch of tests in "parallel".
+ yield Promise.all((
+ for (browser of browsers) BrowserTestUtils.browserLoaded(browser)
+ ));
+ // Flood random browsers with requests. Once promises settle, check that
+ // responses all pass.
+ const results = yield Promise.all((
+ for (browser of randBrowsers(browsers, 50)) ManifestObtainer.browserObtainManifest(browser)
+ ));
+ const pass = results.every(manifest => manifest.name === 'pass');
+ ok(pass, 'Expect every manifest to have name equal to `pass`.');
+ //cleanup
+ browsers
+ .map(browser => gBrowser.getTabForBrowser(browser))
+ .forEach(tab => gBrowser.removeTab(tab));
+
+ //Helper generator, spits out random browsers
+ function* randBrowsers(aBrowsers, aMax) {
+ for (let i = 0; i < aMax; i++) {
+ const randNum = Math.round(Math.random() * (aBrowsers.length - 1));
+ yield aBrowsers[randNum];
+ }
+ }
+});
diff --git a/dom/manifest/test/browser_fire_appinstalled_event.js b/dom/manifest/test/browser_fire_appinstalled_event.js
new file mode 100644
index 000000000..517b120d3
--- /dev/null
+++ b/dom/manifest/test/browser_fire_appinstalled_event.js
@@ -0,0 +1,49 @@
+//Used by JSHint:
+/*global Cu, BrowserTestUtils, ok, add_task, gBrowser */
+"use strict";
+const { PromiseMessage } = Cu.import("resource://gre/modules/PromiseMessage.jsm", {});
+const testPath = "/browser/dom/manifest/test/file_reg_appinstalled_event.html";
+const defaultURL = new URL("http://example.org/browser/dom/manifest/test/file_testserver.sjs");
+const testURL = new URL(defaultURL);
+testURL.searchParams.append("file", testPath);
+
+// Enable window.onappinstalled, so we can fire events at it.
+function enableOnAppInstalledPref() {
+ const ops = {
+ "set": [
+ ["dom.manifest.onappinstalled", true],
+ ],
+ };
+ return SpecialPowers.pushPrefEnv(ops);
+}
+
+// Send a message for the even to be fired.
+// This cause file_reg_install_event.html to be dynamically change.
+function* theTest(aBrowser) {
+ aBrowser.allowEvents = true;
+ let waitForInstall = ContentTask.spawn(aBrowser, null, function*() {
+ yield ContentTaskUtils.waitForEvent(content.window, "appinstalled");
+ });
+ const { data: { success } } = yield PromiseMessage
+ .send(aBrowser.messageManager, "DOM:Manifest:FireAppInstalledEvent");
+ ok(success, "message sent and received successfully.");
+ try {
+ yield waitForInstall;
+ ok(true, "AppInstalled event fired");
+ } catch (err) {
+ ok(false, "AppInstalled event didn't fire: " + err.message);
+ }
+}
+
+// Open a tab and run the test
+add_task(function*() {
+ yield enableOnAppInstalledPref();
+ let tabOptions = {
+ gBrowser: gBrowser,
+ url: testURL.href,
+ };
+ yield BrowserTestUtils.withNewTab(
+ tabOptions,
+ theTest
+ );
+});
diff --git a/dom/manifest/test/common.js b/dom/manifest/test/common.js
new file mode 100644
index 000000000..4f618be80
--- /dev/null
+++ b/dom/manifest/test/common.js
@@ -0,0 +1,22 @@
+/**
+ * Common infrastructure for manifest tests.
+ **/
+/*globals SpecialPowers, ManifestProcessor*/
+'use strict';
+const {
+ ManifestProcessor
+} = SpecialPowers.Cu.import('resource://gre/modules/ManifestProcessor.jsm');
+const processor = ManifestProcessor;
+const manifestURL = new URL(document.location.origin + '/manifest.json');
+const docURL = document.location;
+const seperators = '\u2028\u2029\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000';
+const lineTerminators = '\u000D\u000A\u2028\u2029';
+const whiteSpace = `${seperators}${lineTerminators}`;
+const typeTests = [1, null, {},
+ [], false
+];
+const data = {
+ jsonText: '{}',
+ manifestURL: manifestURL,
+ docURL: docURL
+};
diff --git a/dom/manifest/test/file_reg_appinstalled_event.html b/dom/manifest/test/file_reg_appinstalled_event.html
new file mode 100644
index 000000000..80ff15e11
--- /dev/null
+++ b/dom/manifest/test/file_reg_appinstalled_event.html
@@ -0,0 +1,15 @@
+<meta charset=utf-8>
+<script>
+"use strict";
+window.addEventListener("appinstalled", () => {
+ document
+ .querySelector("#output")
+ .innerHTML = "event received!";
+ // Send a custom event back to the browser
+ // to acknowledge that we got this
+ const detail = { result: true }
+ const ev = new CustomEvent("dom.manifest.onappinstalled", { detail });
+ document.dispatchEvent(ev);
+});
+</script>
+<h1 id=output>waiting for event</h1>
diff --git a/dom/manifest/test/file_testserver.sjs b/dom/manifest/test/file_testserver.sjs
new file mode 100644
index 000000000..5229de9f9
--- /dev/null
+++ b/dom/manifest/test/file_testserver.sjs
@@ -0,0 +1,54 @@
+"use strict";
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+
+function loadHTMLFromFile(path) {
+ // Load the HTML to return in the response from file.
+ // Since it's relative to the cwd of the test runner, we start there and
+ // append to get to the actual path of the file.
+ const testHTMLFile =
+ Components.classes["@mozilla.org/file/directory_service;1"].
+ getService(Components.interfaces.nsIProperties).
+ get("CurWorkD", Components.interfaces.nsILocalFile);
+
+ const testHTMLFileStream =
+ Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+
+ path
+ .split("/")
+ .filter(path => path)
+ .reduce((file, path) => {
+ testHTMLFile.append(path)
+ return testHTMLFile;
+ }, testHTMLFile);
+ testHTMLFileStream.init(testHTMLFile, -1, 0, 0);
+ const isAvailable = testHTMLFileStream.available();
+ return NetUtil.readInputStreamToString(testHTMLFileStream, isAvailable);
+}
+
+function handleRequest(request, response) {
+ const query = new URLSearchParams(request.queryString);
+
+ // avoid confusing cache behaviors
+ response.setHeader("Cache-Control", "no-cache", false);
+
+ // Deliver the CSP policy encoded in the URL
+ if(query.has("csp")){
+ response.setHeader("Content-Security-Policy", query.get("csp"), false);
+ }
+
+ // Deliver the CSPRO policy encoded in the URL
+ if(query.has("cspro")){
+ response.setHeader("Content-Security-Policy-Report-Only", query.get("cspro"), false);
+ }
+
+ // Deliver the CORS header in the URL
+ if(query.has("cors")){
+ response.setHeader("Access-Control-Allow-Origin", query.get("cors"), false);
+ }
+
+ // Send HTML to test allowed/blocked behaviors
+ response.setHeader("Content-Type", "text/html", false);
+ response.write(loadHTMLFromFile(query.get("file")));
+}
diff --git a/dom/manifest/test/manifestLoader.html b/dom/manifest/test/manifestLoader.html
new file mode 100644
index 000000000..e24426090
--- /dev/null
+++ b/dom/manifest/test/manifestLoader.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<meta charset=utf-8>
+<!--
+Uses resource.sjs to load a Web Manifest that can be loaded cross-origin.
+-->
+<link rel="manifest" href='resource.sjs?body={"name":"pass"}&amp;Access-Control-Allow-Origin=*'>
+<h1>Manifest loader</h1>
+<p>Uses resource.sjs to load a Web Manifest that can be loaded cross-origin. The manifest looks like this:</p>
+<pre>
+{
+ "name":"pass"
+}
+</pre>
diff --git a/dom/manifest/test/mochitest.ini b/dom/manifest/test/mochitest.ini
new file mode 100644
index 000000000..24e3b120d
--- /dev/null
+++ b/dom/manifest/test/mochitest.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+ common.js
+ resource.sjs
+ manifestLoader.html
+ file_reg_appinstalled_event.html
+ file_testserver.sjs
+[test_ImageObjectProcessor_sizes.html]
+[test_ImageObjectProcessor_src.html]
+[test_ImageObjectProcessor_type.html]
+[test_ManifestProcessor_background_color.html]
+[test_ManifestProcessor_dir.html]
+[test_ManifestProcessor_display.html]
+[test_ManifestProcessor_icons.html]
+[test_ManifestProcessor_JSON.html]
+[test_ManifestProcessor_lang.html]
+[test_ManifestProcessor_name_and_short_name.html]
+[test_ManifestProcessor_orientation.html]
+[test_ManifestProcessor_scope.html]
+[test_ManifestProcessor_start_url.html]
+[test_ManifestProcessor_theme_color.html]
+[test_ManifestProcessor_warnings.html]
+[test_window_onappinstalled_event.html] \ No newline at end of file
diff --git a/dom/manifest/test/resource.sjs b/dom/manifest/test/resource.sjs
new file mode 100644
index 000000000..ec7804d3f
--- /dev/null
+++ b/dom/manifest/test/resource.sjs
@@ -0,0 +1,85 @@
+/* Generic responder that composes a response from
+ * the query string of a request.
+ *
+ * It reserves some special prop names:
+ * - body: get's used as the response body
+ * - statusCode: override the 200 OK response code
+ * (response text is set automatically)
+ *
+ * Any property names it doesn't know about get converted into
+ * HTTP headers.
+ *
+ * For example:
+ * http://test/resource.sjs?Content-Type=text/html&body=<h1>hello</h1>&Hello=hi
+ *
+ * Outputs:
+ * HTTP/1.1 200 OK
+ * Content-Type: text/html
+ * Hello: hi
+ * <h1>hello</h1>
+ */
+//global handleRequest
+'use strict';
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+const HTTPStatus = new Map([
+ [100, 'Continue'],
+ [101, 'Switching Protocol'],
+ [200, 'OK'],
+ [201, 'Created'],
+ [202, 'Accepted'],
+ [203, 'Non-Authoritative Information'],
+ [204, 'No Content'],
+ [205, 'Reset Content'],
+ [206, 'Partial Content'],
+ [300, 'Multiple Choice'],
+ [301, 'Moved Permanently'],
+ [302, 'Found'],
+ [303, 'See Other'],
+ [304, 'Not Modified'],
+ [305, 'Use Proxy'],
+ [306, 'unused'],
+ [307, 'Temporary Redirect'],
+ [308, 'Permanent Redirect'],
+ [400, 'Bad Request'],
+ [401, 'Unauthorized'],
+ [402, 'Payment Required'],
+ [403, 'Forbidden'],
+ [404, 'Not Found'],
+ [405, 'Method Not Allowed'],
+ [406, 'Not Acceptable'],
+ [407, 'Proxy Authentication Required'],
+ [408, 'Request Timeout'],
+ [409, 'Conflict'],
+ [410, 'Gone'],
+ [411, 'Length Required'],
+ [412, 'Precondition Failed'],
+ [413, 'Request Entity Too Large'],
+ [414, 'Request-URI Too Long'],
+ [415, 'Unsupported Media Type'],
+ [416, 'Requested Range Not Satisfiable'],
+ [417, 'Expectation Failed'],
+ [500, 'Internal Server Error'],
+ [501, 'Not Implemented'],
+ [502, 'Bad Gateway'],
+ [503, 'Service Unavailable'],
+ [504, 'Gateway Timeout'],
+ [505, 'HTTP Version Not Supported']
+]);
+
+function handleRequest(request, response) {
+ const queryMap = new URLSearchParams(request.queryString);
+ if (queryMap.has('statusCode')) {
+ let statusCode = parseInt(queryMap.get('statusCode'));
+ let statusText = HTTPStatus.get(statusCode);
+ queryMap.delete('statusCode');
+ response.setStatusLine('1.1', statusCode, statusText);
+ }
+ if (queryMap.has('body')) {
+ let body = queryMap.get('body') || '';
+ queryMap.delete('body');
+ response.write(decodeURIComponent(body));
+ }
+ for (let [key, value] of queryMap.entries()) {
+ response.setHeader(key, value);
+ }
+}
diff --git a/dom/manifest/test/test_ImageObjectProcessor_sizes.html b/dom/manifest/test/test_ImageObjectProcessor_sizes.html
new file mode 100644
index 000000000..82a8ef991
--- /dev/null
+++ b/dom/manifest/test/test_ImageObjectProcessor_sizes.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * Image object's sizes member
+ * https://w3c.github.io/manifest/#sizes-member
+ **/
+'use strict';
+var validSizes = [{
+ test: '16x16',
+ expect: ['16x16']
+}, {
+ test: 'hello 16x16 16x16',
+ expect: ['16x16']
+}, {
+ test: '32x32 16 48x48 12',
+ expect: ['32x32', '48x48']
+}, {
+ test: `${whiteSpace}128x128${whiteSpace}512x512 8192x8192 32768x32768${whiteSpace}`,
+ expect: ['128x128', '512x512', '8192x8192', '32768x32768']
+}, {
+ test: 'any',
+ expect: ['any']
+}, {
+ test: 'Any',
+ expect: ['Any']
+}, {
+ test: '16x32',
+ expect: ['16x32']
+}, {
+ test: '17x33',
+ expect: ['17x33']
+}, {
+ test: '32x32 32x32',
+ expect: ['32x32']
+}, {
+ test: '32X32',
+ expect: ['32X32']
+}, {
+ test: 'any 32x32',
+ expect: ['any', '32x32']
+}];
+
+var testIcon = {
+ icons: [{
+ src: 'test',
+ sizes: undefined
+ }]
+};
+
+validSizes.forEach(({test, expect}) => {
+ testIcon.icons[0].sizes = test;
+ data.jsonText = JSON.stringify(testIcon);
+ var result = processor.process(data);
+ var sizes = result.icons[0].sizes;
+ var expected = `Expect sizes to equal ${expect.join(" ")}`;
+ is(sizes, expect.join(" "), expected);
+});
+
+var testIcon = {
+ icons: [{
+ src: 'test',
+ sizes: undefined
+ }]
+};
+
+var invalidSizes = ['invalid', '', ' ', '16 x 16', '32', '21', '16xx16', '16 x x 6'];
+invalidSizes.forEach((invalidSize) => {
+ var expected = 'Expect invalid sizes to return undefined.';
+ testIcon.icons[0].sizes = invalidSize;
+ data.jsonText = JSON.stringify(testIcon);
+ var result = processor.process(data);
+ var sizes = result.icons[0].sizes;
+ is(sizes, undefined, expected);
+});
+
+typeTests.forEach((type) => {
+ var expected = `Expect non-string sizes ${typeof type} to be undefined.`;
+ testIcon.icons[0].sizes = type;
+ data.jsonText = JSON.stringify(testIcon);
+ var result = processor.process(data);
+ var sizes = result.icons[0].sizes;
+ is(sizes, undefined, expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ImageObjectProcessor_src.html b/dom/manifest/test/test_ImageObjectProcessor_src.html
new file mode 100644
index 000000000..cb77af0bd
--- /dev/null
+++ b/dom/manifest/test/test_ImageObjectProcessor_src.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * Image object's src member
+ * https://w3c.github.io/manifest/#src-member
+ **/
+'use strict';
+var noSrc = {
+ icons: [{}, {
+ src: []
+ }, {
+ src: {}
+ }, {
+ src: null
+ }, {
+ type: 'image/jpg'
+ }, {
+ sizes: '1x1,2x2'
+ }, {
+ sizes: 'any',
+ type: 'image/jpg'
+ }]
+};
+
+var expected = `Expect icons without a src prop to be filtered out.`;
+data.jsonText = JSON.stringify(noSrc);
+var result = processor.process(data);
+is(result.icons.length, 0, expected);
+
+var invalidSrc = {
+ icons: [{
+ src: null
+ }, {
+ src: 1
+ }, {
+ src: []
+ }, {
+ src: {}
+ }, {
+ src: true
+ }, {
+ src: ''
+ }]
+};
+
+var expected = `Expect icons with invalid src prop to be filtered out.`;
+data.jsonText = JSON.stringify(noSrc);
+var result = processor.process(data);
+is(result.icons.length, 0, expected);
+
+var expected = `Expect icon's src to be a string.`;
+var withSrc = {
+ icons: [{
+ src: 'pass'
+ }]
+};
+data.jsonText = JSON.stringify(withSrc);
+var result = processor.process(data);
+is(typeof result.icons[0].src, "string", expected);
+
+var expected = `Expect only icons with a src prop to be kept.`;
+var withSrc = {
+ icons: [{
+ src: 'pass'
+ }, {
+ src: 'pass',
+ }, {}, {
+ foo: 'foo'
+ }]
+};
+data.jsonText = JSON.stringify(withSrc);
+var result = processor.process(data);
+is(result.icons.length, 2, expected);
+
+var expectedURL = new URL('pass', manifestURL);
+for (var icon of result.icons) {
+ var expected = `Expect src prop to be ${expectedURL.toString()}`;
+ is(icon.src.toString(), expectedURL.toString(), expected);
+}
+
+//Resolve URLs relative to manfiest
+var URLs = ['path', '/path', '../../path'];
+
+URLs.forEach((url) => {
+ var expected = `Resolve icon src URLs relative to manifest.`;
+ data.jsonText = JSON.stringify({
+ icons: [{
+ src: url
+ }]
+ });
+ var absURL = new URL(url, manifestURL).toString();
+ var result = processor.process(data);
+ is(result.icons[0].src.toString(), absURL, expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ImageObjectProcessor_type.html b/dom/manifest/test/test_ImageObjectProcessor_type.html
new file mode 100644
index 000000000..d1b95044d
--- /dev/null
+++ b/dom/manifest/test/test_ImageObjectProcessor_type.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * Image object's type property
+ * https://w3c.github.io/manifest/#type-member
+ **/
+
+'use strict';
+var testIcon = {
+ icons: [{
+ src: 'test',
+ type: undefined
+ }]
+};
+
+var invalidMimeTypes = [
+ 'application / text',
+ 'test;test',
+ ';test?test',
+ 'application\\text',
+ 'image/jpeg, image/gif'
+];
+invalidMimeTypes.forEach((invalidMime) => {
+ var expected = `Expect invalid mime to be treated like undefined.`;
+ testIcon.icons[0].type = invalidMime;
+ data.jsonText = JSON.stringify(testIcon);
+ var result = processor.process(data);
+ is(result.icons[0].type, undefined, expected);
+});
+
+var validTypes = [
+ 'image/jpeg',
+ 'IMAGE/jPeG',
+ `${whiteSpace}image/jpeg${whiteSpace}`,
+ 'image/JPEG; whatever=something',
+ 'image/JPEG;whatever'
+];
+
+validTypes.forEach((validMime) => {
+ var expected = `Expect valid mime to be parsed to : image/jpeg.`;
+ testIcon.icons[0].type = validMime;
+ data.jsonText = JSON.stringify(testIcon);
+ var result = processor.process(data);
+ is(result.icons[0].type, 'image/jpeg', expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_JSON.html b/dom/manifest/test/test_ManifestProcessor_JSON.html
new file mode 100644
index 000000000..0319445eb
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_JSON.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * JSON parsing/processing tests
+ * https://w3c.github.io/manifest/#processing
+ **/
+'use strict';
+var invalidJson = ['', ` \t \n ${whiteSpace} `, '{', '{[[}'];
+invalidJson.forEach((testString) => {
+ var expected = `Expect to recover from invalid JSON: ${testString}`;
+ data.jsonText = testString;
+ var result = processor.process(data);
+ SimpleTest.is(result.start_url, docURL.href, expected);
+});
+
+var validButUnhelpful = ["1", 1, "", "[{}]", "null"];
+validButUnhelpful.forEach((testString) => {
+ var expected = `Expect to recover from invalid JSON: ${testString}`;
+ data.jsonText = testString;
+ var result = processor.process(data);
+ SimpleTest.is(result.start_url, docURL.href, expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_background_color.html b/dom/manifest/test/test_ManifestProcessor_background_color.html
new file mode 100644
index 000000000..e7249df4c
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_background_color.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1195018
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1195018</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * background_color member
+ * https://w3c.github.io/manifest/#background_color-member
+ **/
+'use strict';
+
+typeTests.forEach(type => {
+ data.jsonText = JSON.stringify({
+ background_color: type
+ });
+ var result = processor.process(data);
+
+ is(result.background_color, undefined, `Expect non-string background_color to be undefined: ${typeof type}.`);
+});
+
+var validThemeColors = [
+ 'maroon',
+ '#f00',
+ '#ff0000',
+ 'rgb(255,0,0)',
+ 'rgb(255,0,0,1)',
+ 'rgb(255,0,0,1.0)',
+ 'rgb(255,0,0,100%)',
+ 'rgb(255 0 0)',
+ 'rgb(255 0 0 / 1)',
+ 'rgb(255 0 0 / 1.0)',
+ 'rgb(255 0 0 / 100%)',
+ 'rgb(100%, 0%, 0%)',
+ 'rgb(100%, 0%, 0%, 1)',
+ 'rgb(100%, 0%, 0%, 1.0)',
+ 'rgb(100%, 0%, 0%, 100%)',
+ 'rgb(100% 0% 0%)',
+ 'rgb(100% 0% 0% / 1)',
+ 'rgb(100%, 0%, 0%, 1.0)',
+ 'rgb(100%, 0%, 0%, 100%)',
+ 'rgb(300,0,0)',
+ 'rgb(300 0 0)',
+ 'rgb(255,-10,0)',
+ 'rgb(110%, 0%, 0%)',
+ 'rgba(255,0,0)',
+ 'rgba(255,0,0,1)',
+ 'rgba(255 0 0 / 1)',
+ 'rgba(100%,0%,0%,1)',
+ 'rgba(0,0,255,0.5)',
+ 'rgba(100%, 50%, 0%, 0.1)',
+ 'hsl(120, 100%, 50%)',
+ 'hsl(120 100% 50%)',
+ 'hsl(120, 100%, 50%, 1.0)',
+ 'hsl(120 100% 50% / 1.0)',
+ 'hsla(120, 100%, 50%)',
+ 'hsla(120 100% 50%)',
+ 'hsla(120, 100%, 50%, 1.0)',
+ 'hsla(120 100% 50% / 1.0)',
+ 'hsl(120deg, 100%, 50%)',
+ 'hsl(133.33333333grad, 100%, 50%)',
+ 'hsl(2.0943951024rad, 100%, 50%)',
+ 'hsl(0.3333333333turn, 100%, 50%)',
+];
+
+validThemeColors.forEach(background_color => {
+ data.jsonText = JSON.stringify({
+ background_color: background_color
+ });
+ var result = processor.process(data);
+
+ is(result.background_color, background_color, `Expect background_color to be returned: ${background_color}.`);
+});
+
+var invalidThemeColors = [
+ 'marooon',
+ 'f000000',
+ '#ff00000',
+ 'rgb(100, 0%, 0%)',
+ 'rgb(255,0)',
+ 'rbg(255,-10,0)',
+ 'rgb(110, 0%, 0%)',
+ '(255,0,0) }',
+ 'rgba(255)',
+ ' rgb(100%,0%,0%) }',
+ 'hsl(120, 100%, 50)',
+ 'hsl(120, 100%, 50.0)',
+ 'hsl 120, 100%, 50%',
+ 'hsla{120, 100%, 50%, 1}',
+]
+
+invalidThemeColors.forEach(background_color => {
+ data.jsonText = JSON.stringify({
+ background_color: background_color
+ });
+ var result = processor.process(data);
+
+ is(result.background_color, undefined, `Expect background_color to be undefined: ${background_color}.`);
+});
+
+// Trim tests
+validThemeColors.forEach(background_color => {
+ var expandedThemeColor = `${seperators}${lineTerminators}${background_color}${lineTerminators}${seperators}`;
+ data.jsonText = JSON.stringify({
+ background_color: expandedThemeColor
+ });
+ var result = processor.process(data);
+
+ is(result.background_color, background_color, `Expect trimmed background_color to be returned.`);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_dir.html b/dom/manifest/test/test_ManifestProcessor_dir.html
new file mode 100644
index 000000000..1978eeca3
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_dir.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1258899
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1258899</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * dir member
+ * https://w3c.github.io/manifest/#dir-member
+ **/
+'use strict';
+//Type checks
+typeTests.forEach((type) => {
+ var expected = `Expect non - string dir to default to "auto".`;
+ data.jsonText = JSON.stringify({
+ dir: type
+ });
+ var result = processor.process(data);
+ is(result.dir, 'auto', expected);
+});
+
+/*Test valid values*/
+var validDirs = ['ltr', 'rtl', 'auto']
+validDirs.forEach((dir) => {
+ var expected = `Expect dir value to be ${dir}.`;
+ data.jsonText = JSON.stringify({dir});
+ var result = processor.process(data);
+ is(result.dir, dir, expected);
+});
+
+//trim tests
+validDirs.forEach((dir) => {
+ var expected = `Expect trimmed dir to be returned.`;
+ var expandeddir = seperators + lineTerminators + dir + lineTerminators + seperators;
+ data.jsonText = JSON.stringify({
+ dir: expandeddir
+ });
+ var result = processor.process(data);
+ is(result.dir, dir, expected);
+});
+
+//Unknown/Invalid directions
+var invalidDirs = ['LTR', 'RtL', `fooo${whiteSpace}rtl`, '', 'bar baz, some value', 'ltr rtl auto', 'AuTo'];
+invalidDirs.forEach((dir) => {
+ var expected = `Expect default dir "auto" to be returned: '${dir}'`;
+ data.jsonText = JSON.stringify({dir});
+ var result = processor.process(data);
+ is(result.dir, 'auto', expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_display.html b/dom/manifest/test/test_ManifestProcessor_display.html
new file mode 100644
index 000000000..10106465a
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_display.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * display member
+ * https://w3c.github.io/manifest/#display-member
+ **/
+'use strict';
+//Type checks
+typeTests.forEach((type) => {
+ var expected = `Expect non - string display to default to "browser".`;
+ data.jsonText = JSON.stringify({
+ display: type
+ });
+ var result = processor.process(data);
+ is(result.display, 'browser', expected);
+});
+
+/*Test valid modes - case insensitive*/
+var validModes = [
+ 'fullscreen',
+ 'standalone',
+ 'minimal-ui',
+ 'browser',
+ 'FullScreen',
+ 'standAlone',
+ 'minimal-UI',
+ 'BROWSER',
+]
+validModes.forEach((mode) => {
+ var expected = `Expect display mode to be ${mode.toLowerCase()}.`;
+ data.jsonText = JSON.stringify({
+ display: mode
+ });
+ var result = processor.process(data);
+ is(result.display, mode.toLowerCase(), expected);
+});
+
+//trim tests
+validModes.forEach((display) => {
+ var expected = `Expect trimmed display mode to be returned.`;
+ var expandedDisplay = seperators + lineTerminators + display + lineTerminators + seperators;
+ data.jsonText = JSON.stringify({
+ display: expandedDisplay
+ });
+ var result = processor.process(data);
+ is(result.display, display.toLowerCase(), expected);
+});
+
+//Unknown modes
+var invalidModes = [
+ 'foo',
+ `fooo${whiteSpace}`,
+ '',
+ 'fullscreen,standalone',
+ 'standalone fullscreen',
+ 'FULLSCreENS',
+];
+
+invalidModes.forEach((invalidMode) => {
+ var expected = `Expect default display mode "browser" to be returned: '${invalidMode}'`;
+ data.jsonText = JSON.stringify({
+ display: invalidMode
+ });
+ var result = processor.process(data);
+ is(result.display, 'browser', expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_icons.html b/dom/manifest/test/test_ManifestProcessor_icons.html
new file mode 100644
index 000000000..9bd3d90ec
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_icons.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * Manifest icons member
+ * https://w3c.github.io/manifest/#icons-member
+ **/
+
+'use strict';
+
+typeTests.forEach((type) => {
+ var expected = `Expect non-array icons to be empty array: ${typeof type}.`;
+ data.jsonText = JSON.stringify({
+ icons: type
+ });
+ var result = processor.process(data);
+ var y = SpecialPowers.unwrap(result.icons);
+ is(result.icons.length, 0, expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_lang.html b/dom/manifest/test/test_ManifestProcessor_lang.html
new file mode 100644
index 000000000..f5e994175
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_lang.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<!--
+Bug 1143879 - Implement lang member of Web manifest
+https://bugzilla.mozilla.org/show_bug.cgi?id=1143879
+-->
+<meta charset="utf-8">
+<title>Test for Bug 1143879 - Implement lang member of Web manifest</title>
+<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script src="common.js"></script>
+<script>
+/**
+ * lang member
+ * https://w3c.github.io/manifest/#lang-member
+ **/
+/*globals is, typeTests, data, processor, seperators, lineTerminators, todo_is*/
+'use strict';
+// Type checks: checks that only strings are accepted.
+for (var type of typeTests) {
+ var expected = `Expect non-string to be undefined.`;
+ data.jsonText = JSON.stringify({
+ lang: type
+ });
+ var result = processor.process(data);
+ is(result.lang, undefined, expected);
+}
+
+// Test valid language tags - derived from IANA and BCP-47 spec
+// and our Intl.js implementation.
+var validTags = [
+ 'aa', 'ab', 'ae', 'af', 'ak', 'am', 'an', 'ar', 'as', 'av', 'ay', 'az',
+ 'ba', 'be', 'bg', 'bh', 'bi', 'bm', 'bn', 'bo', 'br', 'bs', 'ca', 'ce',
+ 'ch', 'co', 'cr', 'cs', 'cu', 'cv', 'cy', 'da', 'de', 'dv', 'dz', 'ee',
+ 'el', 'en', 'eo', 'es', 'et', 'eu', 'fa', 'ff', 'fi', 'fj', 'fo', 'fr',
+ 'fy', 'ga', 'gd', 'gl', 'gn', 'gu', 'gv', 'ha', 'he', 'hi', 'ho', 'hr',
+ 'ht', 'hu', 'hy', 'hz', 'ia', 'id', 'ie', 'ig', 'ik', 'in', 'io',
+ 'is', 'it', 'iu', 'iw', 'ja', 'ji', 'jv', 'jw', 'ka', 'kg', 'ki', 'kj',
+ 'kk', 'kl', 'km', 'kn', 'ko', 'kr', 'ks', 'ku', 'kv', 'kw', 'ky', 'la',
+ 'lb', 'lg', 'li', 'ln', 'lo', 'lt', 'lu', 'lv', 'mg', 'mh', 'mi', 'mk',
+ 'ml', 'mn', 'mo', 'mr', 'ms', 'mt', 'my', 'na', 'nb', 'nd', 'ne', 'ng',
+ 'nl', 'nn', 'no', 'nr', 'nv', 'ny', 'oc', 'oj', 'om', 'or', 'os', 'pa',
+ 'pi', 'pl', 'ps', 'pt', 'qu', 'rm', 'rn', 'ro', 'ru', 'rw', 'sa', 'sc',
+ 'sd', 'se', 'sg', 'sh', 'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr',
+ 'ss', 'st', 'su', 'sv', 'sw', 'ta', 'te', 'tg', 'th', 'ti', 'tk', 'tl',
+ 'tn', 'to', 'tr', 'ts', 'tt', 'tw', 'ty', 'ug', 'uk', 'ur', 'uz', 've',
+ 'vi', 'vo', 'wa', 'wo', 'xh', 'yi', 'yo', 'za', 'zh', 'zu', 'en-US',
+ 'jp-JS', 'pt-PT', 'pt-BR', 'de-CH', 'de-DE-1901', 'es-419', 'sl-IT-nedis',
+ 'en-US-boont', 'mn-Cyrl-MN', 'x-fr-CH', 'sr-Cyrl', 'sr-Latn',
+ 'hy-Latn-IT-arevela', 'zh-TW', 'en-GB-boont-r-extended-sequence-x-private',
+ 'zh-nan-hans-bu-variant2-variant1-u-ca-chinese-t-zh-latn-x-private',
+ 'zh-cmn-Hans-CN', 'cmn-Hans-CN', 'zh-yue-HK', 'yue-HK',
+ 'de-CH-x-phonebk', 'az-Arab-x-AZE-derbend', 'x-whatever',
+ 'qaa-Qaaa-QM-x-southern'
+];
+for (var tag of validTags) {
+ var expected = `Expect lang to be ${tag}.`;
+ data.jsonText = JSON.stringify({
+ lang: tag
+ });
+ var result = processor.process(data);
+ is(result.lang, tag, expected);
+}
+
+// trim tests - check that language tags get trimmed properly.
+for (var tag of validTags) {
+ var expected = `Expect trimmed tag to be returned.`;
+ var expandedtag = seperators + lineTerminators + tag;
+ expandedtag += lineTerminators + seperators;
+ data.jsonText = JSON.stringify({
+ lang: expandedtag
+ });
+ var result = processor.process(data);
+ is(result.lang, tag, expected);
+}
+
+//Invalid language tags, derived from BCP-47 and made up.
+var invalidTags = [
+ 'de-419-DE', ' a-DE ', 'ar-a-aaa-b-bbb-a-ccc', 'sdafsdfaadsfdsf', 'i',
+ 'i-phone', 'en US', 'EN-*-US-JP', 'JA-INVALID-TAG', '123123123'
+];
+for (var item of invalidTags) {
+ var expected = `Expect invalid tag (${item}) to be treated as undefined.`;
+ data.jsonText = JSON.stringify({
+ lang: item
+ });
+ var result = processor.process(data);
+ todo_is(result.lang, undefined, expected);
+}
+
+// Canonical form conversion tests. We convert the following tags, which are in
+// canonical form, to upper case and expect the processor to return them
+// in canonical form.
+var canonicalTags = [
+ 'jp-JS', 'pt-PT', 'pt-BR', 'de-CH', 'de-DE-1901', 'es-419', 'sl-IT-nedis',
+ 'en-US-boont', 'mn-Cyrl-MN', 'x-fr-CH', 'sr-Cyrl', 'sr-Latn',
+ 'hy-Latn-IT-arevela', 'zh-TW', 'en-GB-boont-r-extended-sequence-x-private',
+ 'zh-cmn-Hans-CN', 'cmn-Hans-CN', 'zh-yue-HK', 'yue-HK',
+ 'de-CH-x-phonebk', 'az-Arab-x-AZE-derbend', 'x-whatever',
+ 'qaa-Qaaa-QM-x-southern'
+];
+
+for (var tag of canonicalTags) {
+ var uppedTag = tag.toUpperCase();
+ var expected = `Expect tag (${uppedTag}) to be in canonical form (${tag}).`;
+ data.jsonText = JSON.stringify({
+ lang: uppedTag
+ });
+ var result = processor.process(data);
+ todo_is(result.lang, tag, expected);
+}
+
+</script>
diff --git a/dom/manifest/test/test_ManifestProcessor_name_and_short_name.html b/dom/manifest/test/test_ManifestProcessor_name_and_short_name.html
new file mode 100644
index 000000000..682c8d225
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_name_and_short_name.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * name and short_name members
+ * https://w3c.github.io/manifest/#name-member
+ * https://w3c.github.io/manifest/#short_name-member
+ **/
+
+'use strict';
+
+var trimNamesTests = [
+ `${seperators}pass${seperators}`,
+ `${lineTerminators}pass${lineTerminators}`,
+ `${whiteSpace}pass${whiteSpace}`,
+ //BOM
+ `\uFEFFpass\uFEFF`
+];
+var props = ['name', 'short_name'];
+
+props.forEach((prop) => {
+ trimNamesTests.forEach((trimmableString) => {
+ var assetion = `Expecting ${prop} to be trimmed.`;
+ var obj = {};
+ obj[prop] = trimmableString;
+ data.jsonText = JSON.stringify(obj);
+ var result = processor.process(data);
+ is(result[prop], 'pass', assetion);
+ });
+});
+
+/*
+ * If the object is not a string, it becomes undefined
+ */
+props.forEach((prop) => {
+ typeTests.forEach((type) => {
+ var expected = `Expect non - string ${prop} to be undefined: ${typeof type}`;
+ var obj = {};
+ obj[prop] = type;
+ data.jsonText = JSON.stringify(obj);
+ var result = processor.process(data);
+ SimpleTest.ok(result[prop] === undefined, true, expected);
+ });
+});
+
+/**
+ * acceptable names - including long names
+ */
+var acceptableNames = [
+ 'pass',
+ `pass pass pass pass pass pass pass pass pass pass pass pass pass pass
+ pass pass pass pass pass pass pass pass pass pass pass pass pass pass
+ pass pass pass pass pass pass pass pass pass pass pass pass pass pass
+ pass pass pass pass pass pass pass pass pass pass pass pass`,
+ 'これは許容できる名前です',
+ 'ນີ້ແມ່ນຊື່ທີ່ຍອມຮັບໄດ້'
+];
+
+props.forEach((prop) => {
+ acceptableNames.forEach((name) => {
+ var expected = `Expecting name to be acceptable : ${name}`;
+ var obj = {};
+ obj[prop] = name;
+ data.jsonText = JSON.stringify(obj);
+ var result = processor.process(data);
+ is(result[prop], name, expected);
+ });
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_orientation.html b/dom/manifest/test/test_ManifestProcessor_orientation.html
new file mode 100644
index 000000000..67f19a9ff
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_orientation.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * orientation member
+ * https://w3c.github.io/manifest/#orientation-member
+ **/
+'use strict';
+
+typeTests.forEach((type) => {
+ var expected = `Expect non-string orientation to be empty string : ${typeof type}.`;
+ data.jsonText = JSON.stringify({
+ orientation: type
+ });
+ var result = processor.process(data);
+ is(result.orientation, undefined, expected);
+});
+
+var validOrientations = [
+ 'any',
+ 'natural',
+ 'landscape',
+ 'portrait',
+ 'portrait-primary',
+ 'portrait-secondary',
+ 'landscape-primary',
+ 'landscape-secondary',
+ 'aNy',
+ 'NaTuRal',
+ 'LANDsCAPE',
+ 'PORTRAIT',
+ 'portrait-PRIMARY',
+ 'portrait-SECONDARY',
+ 'LANDSCAPE-primary',
+ 'LANDSCAPE-secondary',
+];
+
+validOrientations.forEach((orientation) => {
+ var expected = `Expect orientation to be returned: ${orientation}.`;
+ data.jsonText = JSON.stringify({ orientation });
+ var result = processor.process(data);
+ is(result.orientation, orientation.toLowerCase(), expected);
+});
+
+var invalidOrientations = [
+ 'all',
+ 'ANYMany',
+ 'NaTuRalle',
+ 'portrait-primary portrait-secondary',
+ 'portrait-primary,portrait-secondary',
+ 'any-natural',
+ 'portrait-landscape',
+ 'primary-portrait',
+ 'secondary-portrait',
+ 'landscape-landscape',
+ 'secondary-primary'
+];
+
+invalidOrientations.forEach((orientation) => {
+ var expected = `Expect orientation to be empty string: ${orientation}.`;
+ data.jsonText = JSON.stringify({ orientation });
+ var result = processor.process(data);
+ is(result.orientation, undefined, expected);
+});
+
+//Trim tests
+validOrientations.forEach((orientation) => {
+ var expected = `Expect trimmed orientation to be returned.`;
+ var expandedOrientation = `${seperators}${lineTerminators}${orientation}${lineTerminators}${seperators}`;
+ data.jsonText = JSON.stringify({
+ orientation: expandedOrientation
+ });
+ var result = processor.process(data);
+ is(result.orientation, orientation.toLowerCase(), expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_scope.html b/dom/manifest/test/test_ManifestProcessor_scope.html
new file mode 100644
index 000000000..b1cc9dbd1
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_scope.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * Manifest scope
+ * https://w3c.github.io/manifest/#scope-member
+ **/
+'use strict';
+var expected = 'Expect non-string scope to be undefined';
+typeTests.forEach((type) => {
+ data.jsonText = JSON.stringify({
+ scope: type
+ });
+ var result = processor.process(data);
+ is(result.scope, undefined, expected);
+});
+
+var expected = 'Expect different origin to be treated as undefined';
+data.jsonText = JSON.stringify({
+ scope: 'http://not-same-origin'
+});
+var result = processor.process(data);
+is(result.scope, undefined, expected);
+
+var expected = 'Expect the empty string to be treated as undefined.';
+data.jsonText = JSON.stringify({
+ scope: ''
+});
+var result = processor.process(data);
+is(result.scope, undefined, expected);
+
+var expected = 'Resolve URLs relative to manifest.';
+var URLs = ['path', '/path', '../../path'];
+URLs.forEach((url) => {
+ data.jsonText = JSON.stringify({
+ scope: url,
+ start_url: "/path"
+ });
+ var absURL = new URL(url, manifestURL).toString();
+ var result = processor.process(data);
+ is(result.scope, absURL, expected);
+});
+
+var expected = 'If start URL is not in scope, return undefined.';
+data.jsonText = JSON.stringify({
+ scope: 'foo',
+ start_url: 'bar'
+});
+var result = processor.process(data);
+is(result.scope, undefined, expected);
+
+var expected = 'If start URL is in scope, use the scope.';
+data.jsonText = JSON.stringify({
+ start_url: 'foobar',
+ scope: 'foo'
+});
+var result = processor.process(data);
+is(result.scope.toString(), new URL('foo', manifestURL).toString(), expected);
+
+var expected = 'Expect start_url to be ' + new URL('foobar', manifestURL).toString();
+is(result.start_url.toString(), new URL('foobar', manifestURL).toString(), expected);
+
+var expected = 'If start URL is in scope, use the scope.';
+data.jsonText = JSON.stringify({
+ start_url: '/foo/',
+ scope: '/foo/'
+});
+var result = processor.process(data);
+is(result.scope.toString(), new URL('/foo/', manifestURL).toString(), expected);
+
+var expected = 'If start URL is in scope, use the scope.';
+data.jsonText = JSON.stringify({
+ start_url: '.././foo/',
+ scope: '../foo/'
+});
+var result = processor.process(data);
+is(result.scope.toString(), new URL('/foo/', manifestURL).toString(), expected);
+
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_start_url.html b/dom/manifest/test/test_ManifestProcessor_start_url.html
new file mode 100644
index 000000000..d0b381fa2
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_start_url.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079453
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1079453</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * Manifest start_url
+ * https://w3c.github.io/manifest/#start_url-member
+ **/
+'use strict';
+typeTests.forEach((type) => {
+ var expected = `Expect non - string start_url to be doc's url: ${typeof type}.`;
+ data.jsonText = JSON.stringify({
+ start_url: type
+ });
+ var result = processor.process(data);
+ is(result.start_url.toString(), docURL.toString(), expected);
+});
+
+//Not same origin
+var expected = `Expect different origin URLs to become document's URL.`;
+data.jsonText = JSON.stringify({
+ start_url: 'http://not-same-origin'
+});
+var result = processor.process(data);
+is(result.start_url.toString(), docURL.toString(), expected);
+
+//Empty string test
+var expected = `Expect empty string for start_url to become document's URL.`;
+data.jsonText = JSON.stringify({
+ start_url: ''
+});
+var result = processor.process(data);
+is(result.start_url.toString(), docURL.toString(), expected);
+
+//Resolve URLs relative to manfiest
+var URLs = ['path', '/path', '../../path',
+ `${whiteSpace}path${whiteSpace}`,
+ `${whiteSpace}/path`,
+ `${whiteSpace}../../path`
+];
+URLs.forEach((url) => {
+ var expected = `Resolve URLs relative to manifest.`;
+ data.jsonText = JSON.stringify({
+ start_url: url
+ });
+ var absURL = new URL(url, manifestURL).toString();
+ var result = processor.process(data);
+ is(result.start_url.toString(), absURL, expected);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_theme_color.html b/dom/manifest/test/test_ManifestProcessor_theme_color.html
new file mode 100644
index 000000000..c08830025
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_theme_color.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1195018
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1195018</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+/**
+ * theme_color member
+ * https://w3c.github.io/manifest/#theme_color-member
+ **/
+'use strict';
+
+typeTests.forEach(type => {
+ data.jsonText = JSON.stringify({
+ theme_color: type
+ });
+ var result = processor.process(data);
+
+ is(result.theme_color, undefined, `Expect non-string theme_color to be undefined: ${typeof type}.`);
+});
+
+var validThemeColors = [
+ 'maroon',
+ '#f00',
+ '#ff0000',
+ 'rgb(255,0,0)',
+ 'rgb(255,0,0,1)',
+ 'rgb(255,0,0,1.0)',
+ 'rgb(255,0,0,100%)',
+ 'rgb(255 0 0)',
+ 'rgb(255 0 0 / 1)',
+ 'rgb(255 0 0 / 1.0)',
+ 'rgb(255 0 0 / 100%)',
+ 'rgb(100%, 0%, 0%)',
+ 'rgb(100%, 0%, 0%, 1)',
+ 'rgb(100%, 0%, 0%, 1.0)',
+ 'rgb(100%, 0%, 0%, 100%)',
+ 'rgb(100% 0% 0%)',
+ 'rgb(100% 0% 0% / 1)',
+ 'rgb(100%, 0%, 0%, 1.0)',
+ 'rgb(100%, 0%, 0%, 100%)',
+ 'rgb(300,0,0)',
+ 'rgb(300 0 0)',
+ 'rgb(255,-10,0)',
+ 'rgb(110%, 0%, 0%)',
+ 'rgba(255,0,0)',
+ 'rgba(255,0,0,1)',
+ 'rgba(255 0 0 / 1)',
+ 'rgba(100%,0%,0%,1)',
+ 'rgba(0,0,255,0.5)',
+ 'rgba(100%, 50%, 0%, 0.1)',
+ 'hsl(120, 100%, 50%)',
+ 'hsl(120 100% 50%)',
+ 'hsl(120, 100%, 50%, 1.0)',
+ 'hsl(120 100% 50% / 1.0)',
+ 'hsla(120, 100%, 50%)',
+ 'hsla(120 100% 50%)',
+ 'hsla(120, 100%, 50%, 1.0)',
+ 'hsla(120 100% 50% / 1.0)',
+ 'hsl(120deg, 100%, 50%)',
+ 'hsl(133.33333333grad, 100%, 50%)',
+ 'hsl(2.0943951024rad, 100%, 50%)',
+ 'hsl(0.3333333333turn, 100%, 50%)',
+];
+
+validThemeColors.forEach(theme_color => {
+ data.jsonText = JSON.stringify({
+ theme_color: theme_color
+ });
+ var result = processor.process(data);
+
+ is(result.theme_color, theme_color, `Expect theme_color to be returned: ${theme_color}.`);
+});
+
+var invalidThemeColors = [
+ 'marooon',
+ 'f000000',
+ '#ff00000',
+ 'rgb(100, 0%, 0%)',
+ 'rgb(255,0)',
+ 'rbg(255,-10,0)',
+ 'rgb(110, 0%, 0%)',
+ '(255,0,0) }',
+ 'rgba(255)',
+ ' rgb(100%,0%,0%) }',
+ 'hsl(120, 100%, 50)',
+ 'hsl(120, 100%, 50.0)',
+ 'hsl 120, 100%, 50%',
+ 'hsla{120, 100%, 50%, 1}',
+]
+
+invalidThemeColors.forEach(theme_color => {
+ data.jsonText = JSON.stringify({
+ theme_color: theme_color
+ });
+ var result = processor.process(data);
+
+ is(result.theme_color, undefined, `Expect theme_color to be undefined: ${theme_color}.`);
+});
+
+// Trim tests
+validThemeColors.forEach(theme_color => {
+ var expandedThemeColor = `${seperators}${lineTerminators}${theme_color}${lineTerminators}${seperators}`;
+ data.jsonText = JSON.stringify({
+ theme_color: expandedThemeColor
+ });
+ var result = processor.process(data);
+
+ is(result.theme_color, theme_color, `Expect trimmed theme_color to be returned.`);
+});
+ </script>
+</head>
diff --git a/dom/manifest/test/test_ManifestProcessor_warnings.html b/dom/manifest/test/test_ManifestProcessor_warnings.html
new file mode 100644
index 000000000..865ef8054
--- /dev/null
+++ b/dom/manifest/test/test_ManifestProcessor_warnings.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1086997
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1086997</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="common.js"></script>
+ <script>
+'use strict';
+
+const {
+ ConsoleAPI
+} = SpecialPowers.Cu.import('resource://gre/modules/Console.jsm');
+
+var warning = null;
+
+var originalWarn = ConsoleAPI.prototype.warn;
+ConsoleAPI.prototype.warn = function(aWarning) {
+ warning = aWarning;
+};
+
+[
+ {
+ func: () => data.jsonText = JSON.stringify(1),
+ warning: 'Manifest should be an object.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify(null),
+ warning: 'Manifest should be an object.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify('a string'),
+ warning: 'Manifest should be an object.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify({
+ scope: 'https://www.mozilla.org',
+ }),
+ warning: 'The scope URL must be same origin as document.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify({
+ scope: 'foo',
+ start_url: 'bar',
+ }),
+ warning: 'The start URL is outside the scope, so the scope is invalid.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify({
+ start_url: 'https://www.mozilla.org',
+ }),
+ warning: 'The start URL must be same origin as document.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify({
+ start_url: 42,
+ }),
+ warning: 'Expected the manifest\u2019s start_url member to be a string.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify({
+ theme_color: '42',
+ }),
+ warning: 'theme_color: 42 is not a valid CSS color.',
+ },
+ {
+ func: () => data.jsonText = JSON.stringify({
+ background_color: '42',
+ }),
+ warning: 'background_color: 42 is not a valid CSS color.',
+ },
+].forEach(function(test) {
+ test.func();
+
+ processor.process(data);
+
+ is(warning, test.warning, 'Correct warning.');
+
+ warning = null;
+ data.manifestURL = manifestURL;
+ data.docURL = docURL;
+});
+
+ConsoleAPI.prototype.warn = originalWarn;
+ </script>
+</head>
diff --git a/dom/manifest/test/test_window_onappinstalled_event.html b/dom/manifest/test/test_window_onappinstalled_event.html
new file mode 100644
index 000000000..af57fbf77
--- /dev/null
+++ b/dom/manifest/test/test_window_onappinstalled_event.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1265279
+-->
+
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1309099 - Web Manifest: Implement window.onappinstalled</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+ <script>
+ "use strict";
+ SimpleTest.waitForExplicitFinish();
+ const finish = SimpleTest.finish.bind(SimpleTest);
+ enableOnAppInstalledPref()
+ .then(createIframe)
+ .then(checkImplementation)
+ .then(checkOnappInstalledEventFired)
+ .then(checkAddEventListenerFires)
+ .then(finish)
+ .catch(err => {
+ ok(false, err.stack);
+ finish();
+ });
+
+ function enableOnAppInstalledPref() {
+ const ops = {
+ "set": [
+ ["dom.manifest.onappinstalled", true],
+ ],
+ };
+ return SpecialPowers.pushPrefEnv(ops);
+ }
+
+ // WebIDL conditional annotations for an interface are evaluate once per
+ // global, so we need to create an iframe to see the effects of calling
+ // enableOnAppInstalledPref().
+ function createIframe() {
+ return new Promise((resolve) => {
+ const iframe = document.createElement("iframe");
+ iframe.src = "about:blank";
+ iframe.onload = () => resolve(iframe.contentWindow);
+ document.body.appendChild(iframe);
+ });
+ }
+
+ // Check that the WebIDL is as expected.
+ function checkImplementation(ifrWindow) {
+ return new Promise((resolve, reject) => {
+ const hasOnAppInstalledProp = ifrWindow.hasOwnProperty("onappinstalled");
+ ok(hasOnAppInstalledProp, "window has own onappinstalled property");
+
+ // no point in continuing
+ if (!hasOnAppInstalledProp) {
+ const err = new Error("No 'onappinstalled' IDL attribute. Aborting early.");
+ return reject(err);
+ }
+ is(ifrWindow.onappinstalled, null, "window install is initially set to null");
+
+ // Check that enumerable, configurable, and has a getter and setter.
+ const objDescriptor = Object.getOwnPropertyDescriptor(ifrWindow, "onappinstalled");
+ ok(objDescriptor.enumerable, "is enumerable");
+ ok(objDescriptor.configurable, "is configurable");
+ ok(objDescriptor.hasOwnProperty("get"), "has getter");
+ ok(objDescriptor.hasOwnProperty("set"), "has setter");
+ resolve(ifrWindow);
+ });
+ }
+
+ // Checks that .onappinstalled receives an event.
+ function checkOnappInstalledEventFired(ifrWindow) {
+ const customEv = new CustomEvent("appinstalled");
+ return new Promise((resolve) => {
+ // Test is we receive the event on `appinstalled`
+ ifrWindow.onappinstalled = ev => {
+ ifrWindow.onappinstalled = null;
+ is(ev, customEv, "The events should be the same event object");
+ resolve(ifrWindow);
+ };
+ ifrWindow.dispatchEvent(customEv);
+ });
+ }
+
+ // Checks that .addEventListener("appinstalled") receives an event.
+ function checkAddEventListenerFires(ifrWindow) {
+ const customEv = new CustomEvent("appinstalled");
+ return new Promise((resolve) => {
+ ifrWindow.addEventListener("appinstalled", function handler(ev) {
+ ifrWindow.removeEventListener("appinstalled", handler);
+ is(ev, customEv, "The events should be the same");
+ resolve(ifrWindow);
+ });
+ ifrWindow.dispatchEvent(customEv);
+ });
+ }
+ </script>
+</head>