diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 987d43bfee..69b1e140bf 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -50,6 +50,8 @@ drupal: dependencies: - core/domready - core/drupalSettings + drupalSettings: + suppressDeprecationErrors: true drupalSettings: version: VERSION diff --git a/core/misc/drupal.es6.js b/core/misc/drupal.es6.js index ea47bb790c..c4a6b4ce8a 100644 --- a/core/misc/drupal.es6.js +++ b/core/misc/drupal.es6.js @@ -42,7 +42,7 @@ window.Drupal = { behaviors: {}, locale: {} }; // JavaScript should be made compatible with libraries other than jQuery by // wrapping it in an anonymous closure. -(function(Drupal, drupalSettings, drupalTranslations) { +(function(Drupal, drupalSettings, drupalTranslations, console, Proxy, Reflect) { /** * Helper to rethrow errors asynchronously. * @@ -541,6 +541,61 @@ window.Drupal = { behaviors: {}, locale: {} }; return window.encodeURIComponent(item).replace(/%2F/g, '/'); }; + /** + * Triggers deprecation error. + * + * Deprecation errors are only triggered if deprecation errors haven't + * been suppressed. + * + * @param {Object} deprecation + * The deprecation options. + * @param {string} deprecation.message + * The deprecation message. + * + * @see https://www.drupal.org/core/deprecation#javascript + */ + Drupal.deprecationError = ({ message }) => { + if ( + drupalSettings.suppressDeprecationErrors === false && + typeof console !== 'undefined' && + console.warn + ) { + console.warn(`[Deprecation] ${message}`); + } + }; + + /** + * Triggers deprecation error when object property is being used. + * + * @param {Object} deprecation + * The deprecation options. + * @param {Object} deprecation.target + * The targeted object. + * @param {string} deprecation.deprecatedProperty + * A key of the deprecated property. + * @param {string} deprecation.message + * The deprecation message. + * @returns {Object} + * + * @see https://www.drupal.org/core/deprecation#javascript + */ + Drupal.deprecatedProperty = ({ target, deprecatedProperty, message }) => { + // Proxy and Reflect are not supported by all browsers. Unsupported browsers + // are ignored since this is a development feature. + if (!Proxy || !Reflect) { + return target; + } + + return new Proxy(target, { + get: (target, key, ...rest) => { + if (key === deprecatedProperty) { + Drupal.deprecationError({ message: message }); + } + return Reflect.get(target, key, ...rest); + }, + }); + }; + /** * Generates the themed representation of a Drupal object. * @@ -583,4 +638,11 @@ window.Drupal = { behaviors: {}, locale: {} }; Drupal.theme.placeholder = function(str) { return `${Drupal.checkPlain(str)}`; }; -})(Drupal, window.drupalSettings, window.drupalTranslations); +})( + Drupal, + window.drupalSettings, + window.drupalTranslations, + window.console, + window.Proxy, + window.Reflect, +); diff --git a/core/misc/drupal.js b/core/misc/drupal.js index 4e3389358e..661d511eef 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -7,7 +7,7 @@ window.Drupal = { behaviors: {}, locale: {} }; -(function (Drupal, drupalSettings, drupalTranslations) { +(function (Drupal, drupalSettings, drupalTranslations, console, Proxy, Reflect) { Drupal.throwError = function (error) { setTimeout(function () { throw error; @@ -173,12 +173,43 @@ window.Drupal = { behaviors: {}, locale: {} }; return window.encodeURIComponent(item).replace(/%2F/g, '/'); }; + Drupal.deprecationError = function (_ref) { + var message = _ref.message; + + if (drupalSettings.suppressDeprecationErrors === false && typeof console !== 'undefined' && console.warn) { + console.warn('[Deprecation] ' + message); + } + }; + + Drupal.deprecatedProperty = function (_ref2) { + var target = _ref2.target, + deprecatedProperty = _ref2.deprecatedProperty, + message = _ref2.message; + + if (!Proxy || !Reflect) { + return target; + } + + return new Proxy(target, { + get: function get(target, key) { + for (var _len = arguments.length, rest = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + rest[_key - 2] = arguments[_key]; + } + + if (key === deprecatedProperty) { + Drupal.deprecationError({ message: message }); + } + return Reflect.get.apply(Reflect, [target, key].concat(rest)); + } + }); + }; + Drupal.theme = function (func) { if (func in Drupal.theme) { var _Drupal$theme; - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; + for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; } return (_Drupal$theme = Drupal.theme)[func].apply(_Drupal$theme, args); @@ -188,4 +219,4 @@ window.Drupal = { behaviors: {}, locale: {} }; Drupal.theme.placeholder = function (str) { return '' + Drupal.checkPlain(str) + ''; }; -})(Drupal, window.drupalSettings, window.drupalTranslations); \ No newline at end of file +})(Drupal, window.drupalSettings, window.drupalTranslations, window.console, window.Proxy, window.Reflect); \ No newline at end of file diff --git a/core/modules/system/tests/modules/js_deprecation_log_test/js/js_deprecation_log.es6.js b/core/modules/system/tests/modules/js_deprecation_log_test/js/js_deprecation_log.es6.js new file mode 100644 index 0000000000..aef44e6b77 --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_log_test/js/js_deprecation_log.es6.js @@ -0,0 +1,21 @@ +/** + * @file + * Testing tools for deprecating JavaScript functions and class properties. + */ +(function() { + if (typeof console !== 'undefined' && console.warn) { + const originalWarnFunction = console.warn; + console.warn = warning => { + let warnings = JSON.parse( + localStorage.getItem('js_deprecation_log_test.warnings') || + JSON.stringify([]), + ); + warnings.push(warning); + localStorage.setItem( + 'js_deprecation_log_test.warnings', + JSON.stringify(warnings), + ); + originalWarnFunction(warning); + }; + } +})(); diff --git a/core/modules/system/tests/modules/js_deprecation_log_test/js/js_deprecation_log.js b/core/modules/system/tests/modules/js_deprecation_log_test/js/js_deprecation_log.js new file mode 100644 index 0000000000..167a1bd8e4 --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_log_test/js/js_deprecation_log.js @@ -0,0 +1,18 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function () { + if (typeof console !== 'undefined' && console.warn) { + var originalWarnFunction = console.warn; + console.warn = function (warning) { + var warnings = JSON.parse(localStorage.getItem('js_deprecation_log_test.warnings') || JSON.stringify([])); + warnings.push(warning); + localStorage.setItem('js_deprecation_log_test.warnings', JSON.stringify(warnings)); + originalWarnFunction(warning); + }; + } +})(); \ No newline at end of file diff --git a/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.info.yml b/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.info.yml new file mode 100644 index 0000000000..b2589c6e2a --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.info.yml @@ -0,0 +1,6 @@ +name: 'JS Deprecation log test' +description: 'Stores all JS deprecation calls to allow JS tests to determine if they have been called.' +type: module +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.libraries.yml b/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.libraries.yml new file mode 100644 index 0000000000..1744815c98 --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.libraries.yml @@ -0,0 +1,6 @@ +deprecation_log: + version: VERSION + js: + js/js_deprecation_log.js: { weight: -100 } + dependencies: + - core/drupal diff --git a/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.module b/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.module new file mode 100644 index 0000000000..a86a2f21fb --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_log_test/js_deprecation_log_test.module @@ -0,0 +1,21 @@ + { + deprecationError({ message: 'This function is deprecated for testing purposes.' }); + }; + const objectWithDeprecatedProperty = deprecatedProperty({ + target: { deprecatedProperty: 'Kitten' }, + deprecatedProperty: 'deprecatedProperty', + message: 'This property is deprecated for testing purposes.', + }); + + behaviors.testDeprecations = { + attach: () => { + deprecatedFunction(); + const deprecatedProperty = + objectWithDeprecatedProperty.deprecatedProperty; + }, + }; +})(Drupal); diff --git a/core/modules/system/tests/modules/js_deprecation_test/js/js_deprecation_test.js b/core/modules/system/tests/modules/js_deprecation_test/js/js_deprecation_test.js new file mode 100644 index 0000000000..b5faec0796 --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_test/js/js_deprecation_test.js @@ -0,0 +1,28 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function (_ref) { + var deprecationError = _ref.deprecationError, + deprecatedProperty = _ref.deprecatedProperty, + behaviors = _ref.behaviors; + + var deprecatedFunction = function deprecatedFunction() { + deprecationError({ message: 'This function is deprecated for testing purposes.' }); + }; + var objectWithDeprecatedProperty = deprecatedProperty({ + target: { deprecatedProperty: 'Kitten' }, + deprecatedProperty: 'deprecatedProperty', + message: 'This property is deprecated for testing purposes.' + }); + + behaviors.testDeprecations = { + attach: function attach() { + deprecatedFunction(); + var deprecatedProperty = objectWithDeprecatedProperty.deprecatedProperty; + } + }; +})(Drupal); \ No newline at end of file diff --git a/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.info.yml b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.info.yml new file mode 100644 index 0000000000..0a1b6153be --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.info.yml @@ -0,0 +1,6 @@ +name: 'JS Deprecation test' +description: 'Provides deprecated code that can be used for tests' +type: module +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.libraries.yml b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.libraries.yml new file mode 100644 index 0000000000..0ae3e7ce3c --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.libraries.yml @@ -0,0 +1,6 @@ +deprecation_test: + version: VERSION + js: + js/js_deprecation_test.js: {} + dependencies: + - core/drupal diff --git a/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module new file mode 100644 index 0000000000..523c286875 --- /dev/null +++ b/core/modules/system/tests/modules/js_deprecation_test/js_deprecation_test.module @@ -0,0 +1,13 @@ + ['library' => ['js_deprecation_test/deprecation_test']], + ]; + } + +} diff --git a/core/profiles/testing/testing.info.yml b/core/profiles/testing/testing.info.yml index 834198a743..317c4f0f9b 100644 --- a/core/profiles/testing/testing.info.yml +++ b/core/profiles/testing/testing.info.yml @@ -9,3 +9,4 @@ install: # tests as possible run with them enabled. - drupal:page_cache - dynamic_page_cache + - js_deprecation_log_test diff --git a/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php new file mode 100644 index 0000000000..4714cecf72 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/JavascriptDeprecationTest.php @@ -0,0 +1,26 @@ +drupalGet('js_deprecation_test'); + // Ensure that deprecation message from previous page loads will be + // detected. + $this->drupalGet('user'); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php index d677aa8e2d..a4f2785cb1 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/WebDriverTestBase.php @@ -76,8 +76,9 @@ protected function initMink() { * {@inheritdoc} */ protected function installModulesFromClassProperty(ContainerInterface $container) { + self::$modules = ['js_deprecation_log_test']; if ($this->disableCssAnimations) { - self::$modules = ['css_disable_transitions_test']; + self::$modules[] = 'css_disable_transitions_test'; } parent::installModulesFromClassProperty($container); } @@ -108,6 +109,13 @@ protected function tearDown() { // explaining what the problem is. throw new \RuntimeException('Unfinished AJAX requests while tearing down a test'); } + + $warnings = $this->getSession()->evaluateScript("JSON.parse(localStorage.getItem('js_deprecation_log_test.warnings') || JSON.stringify([]))"); + foreach ($warnings as $warning) { + if (strpos($warning, '[Deprecation]') === 0) { + @trigger_error('Javascript Deprecation:' . substr($warning, 13), E_USER_DEPRECATED); + } + } } parent::tearDown(); } diff --git a/core/tests/Drupal/Nightwatch/Assertions/deprecationErrorExists.js b/core/tests/Drupal/Nightwatch/Assertions/deprecationErrorExists.js new file mode 100644 index 0000000000..7c15fe06e8 --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Assertions/deprecationErrorExists.js @@ -0,0 +1,22 @@ +module.exports.assertion = function(expected) { + this.message = `Testing if "${expected}" deprecation error has been triggered`; + this.expected = expected; + this.pass = deprecationMessages => { + return deprecationMessages.includes(expected); + }; + this.value = result => { + const localStorageEntries = JSON.parse(result.value); + const deprecationMessages = localStorageEntries !== null ? localStorageEntries.filter(message => { + return new RegExp('\[Deprecation\]').test(message); + }) : []; + + return deprecationMessages.map(message => { + return message.replace('\[Deprecation\] ', ''); + }) + }; + this.command = callback => { + return this.api.execute(function() { + return window.localStorage.getItem('js_deprecation_log_test.warnings'); + }, callback); + }; +}; diff --git a/core/tests/Drupal/Nightwatch/Assertions/noDeprecationErrors.js b/core/tests/Drupal/Nightwatch/Assertions/noDeprecationErrors.js new file mode 100644 index 0000000000..095da1434d --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Assertions/noDeprecationErrors.js @@ -0,0 +1,22 @@ +module.exports.assertion = function() { + this.message = 'Ensuring no deprecation errors have been triggered'; + this.expected = ''; + this.pass = deprecationMessages => { + return deprecationMessages.length === 0; + }; + this.value = result => { + const localStorageEntries = JSON.parse(result.value); + const deprecationMessages = localStorageEntries !== null ? localStorageEntries.filter(message => { + return new RegExp('\[Deprecation\]').test(message); + }) : []; + + return deprecationMessages.map(message => { + return message.replace('\[Deprecation\] ', ''); + }) + }; + this.command = callback => { + return this.api.execute(function() { + return window.localStorage.getItem('js_deprecation_log_test.warnings'); + }, callback); + }; +}; diff --git a/core/tests/Drupal/Nightwatch/Tests/exampleTest.js b/core/tests/Drupal/Nightwatch/Tests/exampleTest.js index 04f21954d0..da58421350 100644 --- a/core/tests/Drupal/Nightwatch/Tests/exampleTest.js +++ b/core/tests/Drupal/Nightwatch/Tests/exampleTest.js @@ -22,6 +22,7 @@ module.exports = { .drupalRelativeURL('/test-page') .waitForElementVisible('@body', testPage.props.timeout) .assert.containsText('@body', testPage.props.text) + .assert.noDeprecationErrors() .drupalLogAndEnd({ onlyOnError: false }); }, }; diff --git a/core/tests/Drupal/Nightwatch/Tests/jsDeprecationTest.js b/core/tests/Drupal/Nightwatch/Tests/jsDeprecationTest.js new file mode 100644 index 0000000000..bbae7134f2 --- /dev/null +++ b/core/tests/Drupal/Nightwatch/Tests/jsDeprecationTest.js @@ -0,0 +1,28 @@ +module.exports = { + '@tags': ['core'], + before(browser) { + browser.drupalInstall().drupalLoginAsAdmin(() => { + browser + .drupalRelativeURL('/admin/modules') + .setValue('input[type="search"]', 'JS Deprecation test') + .waitForElementVisible( + 'input[name="modules[js_deprecation_test][enable]"]', + 1000, + ) + .click('input[name="modules[js_deprecation_test][enable]"]') + .click('input[type="submit"]'); // Submit module form. + }); + }, + after(browser) { + browser.drupalUninstall(); + }, + 'Test JavaScript deprecations': browser => { + browser + .drupalRelativeURL('/js_deprecation_test') + .waitForElementVisible('body', 1000) + .assert.containsText('h1.page-title', 'JsDeprecationTest') + .assert.deprecationErrorExists('This function is deprecated for testing purposes.') + .assert.deprecationErrorExists('This property is deprecated for testing purposes.') + .drupalLogAndEnd({ onlyOnError: false }); + }, +}; diff --git a/core/tests/Drupal/Nightwatch/Tests/loginTest.js b/core/tests/Drupal/Nightwatch/Tests/loginTest.js index 3f1bb0a971..5008e906c4 100644 --- a/core/tests/Drupal/Nightwatch/Tests/loginTest.js +++ b/core/tests/Drupal/Nightwatch/Tests/loginTest.js @@ -17,7 +17,8 @@ module.exports = { }) .drupalLogin({ name: 'user', password: '123' }) .drupalRelativeURL('/admin/reports') - .expect.element('h1.page-title') - .text.to.contain('Reports'); + .waitForElementVisible('body', 1000) + .assert.containsText('h1.page-title', 'Reports') + .assert.noDeprecationErrors(); }, }; diff --git a/core/tests/Drupal/Nightwatch/Tests/statesTest.js b/core/tests/Drupal/Nightwatch/Tests/statesTest.js index f7ee1ba522..35bdd760a5 100644 --- a/core/tests/Drupal/Nightwatch/Tests/statesTest.js +++ b/core/tests/Drupal/Nightwatch/Tests/statesTest.js @@ -18,6 +18,7 @@ module.exports = { browser .drupalRelativeURL('/form-test/javascript-states-form') .waitForElementVisible('body', 1000) - .waitForElementNotVisible('input[name="textfield"]', 1000); + .waitForElementNotVisible('input[name="textfield"]', 1000) + .assert.noDeprecationErrors(); }, };