diff options
Diffstat (limited to 'devtools/server/actors/compatibility')
8 files changed, 732 insertions, 0 deletions
diff --git a/devtools/server/actors/compatibility/compatibility.js b/devtools/server/actors/compatibility/compatibility.js new file mode 100644 index 0000000000..21ff68b310 --- /dev/null +++ b/devtools/server/actors/compatibility/compatibility.js @@ -0,0 +1,162 @@ +/* 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/. */ + +"use strict"; + +const { Actor } = require("resource://devtools/shared/protocol.js"); +const { + compatibilitySpec, +} = require("resource://devtools/shared/specs/compatibility.js"); + +loader.lazyGetter(this, "mdnCompatibility", () => { + const MDNCompatibility = require("resource://devtools/server/actors/compatibility/lib/MDNCompatibility.js"); + const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json"); + return new MDNCompatibility(cssPropertiesCompatData); +}); + +class CompatibilityActor extends Actor { + /** + * Create a CompatibilityActor. + * CompatibilityActor is responsible for providing the compatibility information + * for the web page using the data from the Inspector and the `MDNCompatibility` + * and conveys them to the compatibility panel in the DevTool Inspector. Currently, + * the `CompatibilityActor` only detects compatibility issues in the CSS declarations + * but plans are in motion to extend it to evaluate compatibility information for + * HTML and JavaScript. + * The design below has the InspectorActor own the CompatibilityActor, but it's + * possible we will want to move it into it's own panel in the future. + * + * @param inspector + * The InspectorActor that owns this CompatibilityActor. + * + * @constructor + */ + constructor(inspector) { + super(inspector.conn, compatibilitySpec); + this.inspector = inspector; + } + + destroy() { + super.destroy(); + this.inspector = null; + } + + form() { + return { + actor: this.actorID, + }; + } + + getTraits() { + return { + traits: {}, + }; + } + + /** + * Responsible for computing the compatibility issues for a list of CSS declaration blocks + * + * @param {Array<Array<Object>>} domRulesDeclarations: An array of arrays of CSS declaration object + * @param {string} domRulesDeclarations[][].name: Declaration name + * @param {string} domRulesDeclarations[][].value: Declaration value + * @param {Array<Object>} targetBrowsers: Array of target browsers () to be used to check CSS compatibility against + * @param {string} targetBrowsers[].id: Browser id as specified in `devtools/shared/compatibility/datasets/browser.json` + * @param {string} targetBrowsers[].name + * @param {string} targetBrowsers[].version + * @param {string} targetBrowsers[].status: Browser status - esr, current, beta, nightly + * @returns {Array<Array<Object>>} An Array of arrays of JSON objects with compatibility + * information in following form: + * { + * // Type of compatibility issue + * type: <string>, + * // The CSS declaration that has compatibility issues + * property: <string>, + * // Alias to the given CSS property + * alias: <Array>, + * // Link to MDN documentation for the particular CSS rule + * url: <string>, + * deprecated: <boolean>, + * experimental: <boolean>, + * // An array of all the browsers that don't support the given CSS rule + * unsupportedBrowsers: <Array>, + * } + */ + getCSSDeclarationBlockIssues(domRulesDeclarations, targetBrowsers) { + return domRulesDeclarations.map(declarationBlock => + mdnCompatibility.getCSSDeclarationBlockIssues( + declarationBlock, + targetBrowsers + ) + ); + } + + /** + * Responsible for computing the compatibility issues in the + * CSS declaration of the given node. + * @param NodeActor node + * @param targetBrowsers Array + * An Array of JSON object of target browser to check compatibility against in following form: + * { + * // Browser id as specified in `devtools/server/actors/compatibility/lib/datasets/browser.json` + * id: <string>, + * name: <string>, + * version: <string>, + * // Browser status - esr, current, beta, nightly + * status: <string>, + * } + * @returns An Array of JSON objects with compatibility information in following form: + * { + * // Type of compatibility issue + * type: <string>, + * // The CSS declaration that has compatibility issues + * property: <string>, + * // Alias to the given CSS property + * alias: <Array>, + * // Link to MDN documentation for the particular CSS rule + * url: <string>, + * deprecated: <boolean>, + * experimental: <boolean>, + * // An array of all the browsers that don't support the given CSS rule + * unsupportedBrowsers: <Array>, + * } + */ + async getNodeCssIssues(node, targetBrowsers) { + const pageStyle = await this.inspector.getPageStyle(); + const styles = await pageStyle.getApplied(node, { + skipPseudo: false, + }); + + const declarationBlocks = styles.entries + .map(({ rule }) => { + // Replace form() with a function to get minimal subset + // of declarations from StyleRuleActor when such a + // function lands in the StyleRuleActor + const declarations = rule.form().declarations; + if (!declarations) { + return null; + } + return declarations.filter(d => !d.commentOffsets); + }) + .filter(declarations => declarations && declarations.length); + + return declarationBlocks + .map(declarationBlock => + mdnCompatibility.getCSSDeclarationBlockIssues( + declarationBlock, + targetBrowsers + ) + ) + .flat() + .reduce((issues, issue) => { + // Get rid of duplicate issue + return issues.find( + i => i.type === issue.type && i.property === issue.property + ) + ? issues + : [...issues, issue]; + }, []); + } +} + +exports.CompatibilityActor = CompatibilityActor; diff --git a/devtools/server/actors/compatibility/lib/MDNCompatibility.js b/devtools/server/actors/compatibility/lib/MDNCompatibility.js new file mode 100644 index 0000000000..9975123103 --- /dev/null +++ b/devtools/server/actors/compatibility/lib/MDNCompatibility.js @@ -0,0 +1,327 @@ +/* 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/. */ + +"use strict"; + +const _SUPPORT_STATE_BROWSER_NOT_FOUND = "BROWSER_NOT_FOUND"; +const _SUPPORT_STATE_SUPPORTED = "SUPPORTED"; +const _SUPPORT_STATE_UNSUPPORTED = "UNSUPPORTED"; +const _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED = "UNSUPPORTED_PREFIX_NEEDED"; + +loader.lazyRequireGetter( + this, + "COMPATIBILITY_ISSUE_TYPE", + "resource://devtools/shared/constants.js", + true +); + +loader.lazyRequireGetter( + this, + ["getCompatNode", "getCompatTable"], + "resource://devtools/shared/compatibility/helpers.js", + true +); + +const PREFIX_REGEX = /^-\w+-/; + +/** + * A class with methods used to query the MDN compatibility data for CSS properties and + * HTML nodes and attributes for specific browsers and versions. + */ +class MDNCompatibility { + /** + * Constructor. + * + * @param {JSON} cssPropertiesCompatData + * JSON of the compat data for CSS properties. + * https://github.com/mdn/browser-compat-data/tree/master/css/properties + */ + constructor(cssPropertiesCompatData) { + this._cssPropertiesCompatData = cssPropertiesCompatData; + } + + /** + * Return the CSS related compatibility issues from given CSS declaration blocks. + * + * @param {Array} declarations + * CSS declarations to check. + * e.g. [{ name: "background-color", value: "lime" }, ...] + * @param {Array} browsers + * Restrict compatibility checks to these browsers and versions. + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @return {Array} issues + */ + getCSSDeclarationBlockIssues(declarations, browsers) { + const summaries = []; + for (const { name: property } of declarations) { + // Ignore CSS custom properties as any name is valid. + if (property.startsWith("--")) { + continue; + } + + summaries.push(this._getCSSPropertyCompatSummary(browsers, property)); + } + + // Classify to aliases summaries and normal summaries. + const { aliasSummaries, normalSummaries } = + this._classifyCSSCompatSummaries(summaries, browsers); + + // Finally, convert to CSS issues. + return this._toCSSIssues(normalSummaries.concat(aliasSummaries)); + } + + /** + * Classify the compatibility summaries that are able to get from + * `getCSSPropertyCompatSummary`. + * There are CSS properties that can specify the style with plural aliases such as + * `user-select`, aggregates those as the aliases summaries. + * + * @param {Array} summaries + * Assume the result of _getCSSPropertyCompatSummary(). + * @param {Array} browsers + * All browsers that to check + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @return Object + * { + * aliasSummaries: Array of alias summary, + * normalSummaries: Array of normal summary + * } + */ + _classifyCSSCompatSummaries(summaries, browsers) { + const aliasSummariesMap = new Map(); + const normalSummaries = summaries.filter(s => { + const { + database, + invalid, + terms, + unsupportedBrowsers, + prefixNeededBrowsers, + } = s; + + if (invalid) { + return true; + } + + const alias = this._getAlias(database, terms); + if (!alias) { + return true; + } + + if (!aliasSummariesMap.has(alias)) { + aliasSummariesMap.set( + alias, + Object.assign(s, { + property: alias, + aliases: [], + unsupportedBrowsers: browsers, + prefixNeededBrowsers: browsers, + }) + ); + } + + // Update alias summary. + const terminal = terms.pop(); + const aliasSummary = aliasSummariesMap.get(alias); + if (!aliasSummary.aliases.includes(terminal)) { + aliasSummary.aliases.push(terminal); + } + aliasSummary.unsupportedBrowsers = + aliasSummary.unsupportedBrowsers.filter(b => + unsupportedBrowsers.includes(b) + ); + aliasSummary.prefixNeededBrowsers = + aliasSummary.prefixNeededBrowsers.filter(b => + prefixNeededBrowsers.includes(b) + ); + return false; + }); + + const aliasSummaries = [...aliasSummariesMap.values()].map(s => { + s.prefixNeeded = s.prefixNeededBrowsers.length !== 0; + return s; + }); + + return { aliasSummaries, normalSummaries }; + } + + _getAlias(compatNode, terms) { + const targetNode = getCompatNode(compatNode, terms); + return targetNode ? targetNode._aliasOf : null; + } + + /** + * Return the compatibility summary of the terms. + * + * @param {Array} browsers + * All browsers that to check + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @param {Array} database + * MDN compatibility dataset where finds from + * @param {Array} terms + * The terms which is checked the compatibility summary from the + * database. The paremeters are passed as `rest parameters`. + * e.g. _getCompatSummary(browsers, database, "user-select", ...) + * @return {Object} + * { + * database: The passed database as a parameter, + * terms: The passed terms as a parameter, + * url: The link which indicates the spec in MDN, + * deprecated: true if the spec of terms is deprecated, + * experimental: true if the spec of terms is experimental, + * unsupportedBrowsers: Array of unsupported browsers, + * } + */ + _getCompatSummary(browsers, database, terms) { + const compatTable = getCompatTable(database, terms); + + if (!compatTable) { + return { invalid: true, unsupportedBrowsers: [] }; + } + + const unsupportedBrowsers = []; + const prefixNeededBrowsers = []; + + for (const browser of browsers) { + const state = this._getSupportState( + compatTable, + browser, + database, + terms + ); + + switch (state) { + case _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED: { + prefixNeededBrowsers.push(browser); + unsupportedBrowsers.push(browser); + break; + } + case _SUPPORT_STATE_UNSUPPORTED: { + unsupportedBrowsers.push(browser); + break; + } + } + } + + const { deprecated, experimental } = compatTable.status || {}; + + return { + database, + terms, + url: compatTable.mdn_url, + specUrl: compatTable.spec_url, + deprecated, + experimental, + unsupportedBrowsers, + prefixNeededBrowsers, + }; + } + + /** + * Return the compatibility summary of the CSS property. + * This function just adds `property` filed to the result of `_getCompatSummary`. + * + * @param {Array} browsers + * All browsers that to check + * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] + * @return {Object} compatibility summary + */ + _getCSSPropertyCompatSummary(browsers, property) { + const summary = this._getCompatSummary( + browsers, + this._cssPropertiesCompatData, + [property] + ); + return Object.assign(summary, { property }); + } + + _getSupportState(compatTable, browser, compatNode, terms) { + const supportList = compatTable.support[browser.id]; + if (!supportList) { + return _SUPPORT_STATE_BROWSER_NOT_FOUND; + } + + const version = parseFloat(browser.version); + const terminal = terms.at(-1); + const prefix = terminal.match(PREFIX_REGEX)?.[0]; + + let prefixNeeded = false; + for (const support of supportList) { + const { alternative_name: alternativeName, added, removed } = support; + + if ( + // added id true when feature is supported, but we don't know the version + (added === true || + // `null` and `undefined` is when we don't know if it's supported. + // Since we don't want to have false negative, we consider it as supported + added === null || + added === undefined || + // It was added on a previous version number + added <= version) && + // `added` is false when the property isn't supported + added !== false && + // `removed` is false when the feature wasn't removevd + (removed === false || + // `null` and `undefined` is when we don't know if it was removed. + // Since we don't want to have false negative, we consider it as supported + removed === null || + removed === undefined || + // It was removed, but on a later version, so it's still supported + version <= removed) + ) { + if (alternativeName) { + if (alternativeName === terminal) { + return _SUPPORT_STATE_SUPPORTED; + } + } else if ( + support.prefix === prefix || + // There are compat data that are defined with prefix like "-moz-binding". + // In this case, we don't have to check the prefix. + (prefix && !this._getAlias(compatNode, terms)) + ) { + return _SUPPORT_STATE_SUPPORTED; + } + + prefixNeeded = true; + } + } + + return prefixNeeded + ? _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED + : _SUPPORT_STATE_UNSUPPORTED; + } + + _hasIssue({ unsupportedBrowsers, deprecated, experimental, invalid }) { + // Don't apply as issue the invalid term which was not in the database. + return ( + !invalid && (unsupportedBrowsers.length || deprecated || experimental) + ); + } + + _toIssue(summary, type) { + const issue = Object.assign({}, summary, { type }); + delete issue.database; + delete issue.terms; + delete issue.prefixNeededBrowsers; + return issue; + } + + _toCSSIssues(summaries) { + const issues = []; + + for (const summary of summaries) { + if (!this._hasIssue(summary)) { + continue; + } + + const type = summary.aliases + ? COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES + : COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY; + issues.push(this._toIssue(summary, type)); + } + + return issues; + } +} + +module.exports = MDNCompatibility; diff --git a/devtools/server/actors/compatibility/lib/moz.build b/devtools/server/actors/compatibility/lib/moz.build new file mode 100644 index 0000000000..f28d8fe482 --- /dev/null +++ b/devtools/server/actors/compatibility/lib/moz.build @@ -0,0 +1,11 @@ +# -*- 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/. + +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"] + +DevToolsModules( + "MDNCompatibility.js", +) diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/.eslintrc.js b/devtools/server/actors/compatibility/lib/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..65efbdee13 --- /dev/null +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + // Extend from the common devtools xpcshell eslintrc config. + extends: "../../../../../../.eslintrc.xpcshell.js", +}; diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/head.js b/devtools/server/actors/compatibility/lib/test/xpcshell/head.js new file mode 100644 index 0000000000..733c0400da --- /dev/null +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/head.js @@ -0,0 +1,10 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +const { require } = ChromeUtils.importESModule( + "resource://devtools/shared/loader/Loader.sys.mjs" +); diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js b/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js new file mode 100644 index 0000000000..e411feb3b0 --- /dev/null +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/test_mdn-compatibility.js @@ -0,0 +1,193 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test for the MDN compatibility diagnosis module. + +const { + COMPATIBILITY_ISSUE_TYPE, +} = require("resource://devtools/shared/constants.js"); +const MDNCompatibility = require("resource://devtools/server/actors/compatibility/lib/MDNCompatibility.js"); +const cssPropertiesCompatData = require("resource://devtools/shared/compatibility/dataset/css-properties.json"); + +const mdnCompatibility = new MDNCompatibility(cssPropertiesCompatData); + +const FIREFOX_1 = { + id: "firefox", + version: "1", +}; + +const FIREFOX_60 = { + id: "firefox", + version: "60", +}; + +const FIREFOX_69 = { + id: "firefox", + version: "69", +}; + +const FIREFOX_ANDROID_1 = { + id: "firefox_android", + version: "1", +}; + +const SAFARI_13 = { + id: "safari", + version: "13", +}; + +const TEST_DATA = [ + { + description: "Test for a supported property", + declarations: [{ name: "background-color" }], + browsers: [FIREFOX_69], + expectedIssues: [], + }, + { + description: "Test for some supported properties", + declarations: [{ name: "background-color" }, { name: "color" }], + browsers: [FIREFOX_69], + expectedIssues: [], + }, + { + description: "Test for an unsupported property", + declarations: [{ name: "grid-column" }], + browsers: [FIREFOX_1], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "grid-column", + url: "https://developer.mozilla.org/docs/Web/CSS/grid-column", + specUrl: "https://drafts.csswg.org/css-grid/#placement-shorthands", + deprecated: false, + experimental: false, + unsupportedBrowsers: [FIREFOX_1], + }, + ], + }, + { + description: "Test for an unknown property", + declarations: [{ name: "unknown-property" }], + browsers: [FIREFOX_69], + expectedIssues: [], + }, + { + description: "Test for a deprecated property", + declarations: [{ name: "clip" }], + browsers: [FIREFOX_69], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "clip", + url: "https://developer.mozilla.org/docs/Web/CSS/clip", + specUrl: "https://drafts.fxtf.org/css-masking/#clip-property", + deprecated: true, + experimental: false, + unsupportedBrowsers: [], + }, + ], + }, + { + description: "Test for a property having some issues", + declarations: [{ name: "ruby-align" }], + browsers: [FIREFOX_1], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "ruby-align", + url: "https://developer.mozilla.org/docs/Web/CSS/ruby-align", + specUrl: "https://drafts.csswg.org/css-ruby/#ruby-align-property", + deprecated: false, + experimental: true, + unsupportedBrowsers: [FIREFOX_1], + }, + ], + }, + { + description: + "Test for an aliased property not supported in all browsers with prefix needed", + declarations: [{ name: "-moz-user-select" }], + browsers: [FIREFOX_69, SAFARI_13], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES, + property: "user-select", + aliases: ["-moz-user-select"], + url: "https://developer.mozilla.org/docs/Web/CSS/user-select", + specUrl: "https://drafts.csswg.org/css-ui/#content-selection", + deprecated: false, + experimental: false, + prefixNeeded: true, + unsupportedBrowsers: [SAFARI_13], + }, + ], + }, + { + description: + "Test for an aliased property not supported in all browsers without prefix needed", + declarations: [ + { name: "-moz-user-select" }, + { name: "-webkit-user-select" }, + ], + browsers: [FIREFOX_ANDROID_1, FIREFOX_69, SAFARI_13], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES, + property: "user-select", + aliases: ["-moz-user-select", "-webkit-user-select"], + url: "https://developer.mozilla.org/docs/Web/CSS/user-select", + specUrl: "https://drafts.csswg.org/css-ui/#content-selection", + deprecated: false, + experimental: false, + prefixNeeded: false, + unsupportedBrowsers: [FIREFOX_ANDROID_1], + }, + ], + }, + { + description: "Test for aliased properties supported in all browsers", + declarations: [ + { name: "-moz-user-select" }, + { name: "-webkit-user-select" }, + ], + browsers: [FIREFOX_69, SAFARI_13], + expectedIssues: [], + }, + { + description: "Test for a property defined with prefix", + declarations: [{ name: "-moz-user-input" }], + browsers: [FIREFOX_1, FIREFOX_60, FIREFOX_69], + expectedIssues: [ + { + type: COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY, + property: "-moz-user-input", + url: "https://developer.mozilla.org/docs/Web/CSS/-moz-user-input", + specUrl: undefined, + deprecated: true, + experimental: false, + unsupportedBrowsers: [], + }, + ], + }, +]; + +add_task(() => { + for (const { + description, + declarations, + browsers, + expectedIssues, + } of TEST_DATA) { + info(description); + const issues = mdnCompatibility.getCSSDeclarationBlockIssues( + declarations, + browsers + ); + deepEqual( + issues, + expectedIssues, + "CSS declaration compatibility data matches expectations" + ); + } +}); diff --git a/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.ini b/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..490afa9504 --- /dev/null +++ b/devtools/server/actors/compatibility/lib/test/xpcshell/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +tags = devtools +head = head.js +firefox-appdir = browser +skip-if = toolkit == 'android' + +[test_mdn-compatibility.js] diff --git a/devtools/server/actors/compatibility/moz.build b/devtools/server/actors/compatibility/moz.build new file mode 100644 index 0000000000..010b027d37 --- /dev/null +++ b/devtools/server/actors/compatibility/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/. + +DIRS += [ + "lib", +] + +DevToolsModules( + "compatibility.js", +) + +with Files("**"): + BUG_COMPONENT = ("DevTools", "Inspector: Compatibility") |