diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/manifest | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/manifest')
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"}&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> |