Issue #3070521 by lauriii, tedbow, Wim Leers, larowlan, justafish, zrpnr, alexpott, catch: Trigger deprecation notifications on JavaScript deprecations to notify developers that deprecated code is being used

merge-requests/2419/head
Lee Rowlands 2019-10-11 10:34:41 +10:00
parent 6ea36484fb
commit 1b0c006ac9
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
27 changed files with 422 additions and 12 deletions

View File

@ -50,6 +50,8 @@ drupal:
dependencies:
- core/domready
- core/drupalSettings
drupalSettings:
suppressDeprecationErrors: true
drupalSettings:
version: VERSION

View File

@ -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 });
}
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 `<em class="placeholder">${Drupal.checkPlain(str)}</em>`;
};
})(Drupal, window.drupalSettings, window.drupalTranslations);
})(
Drupal,
window.drupalSettings,
window.drupalTranslations,
window.console,
window.Proxy,
window.Reflect,
);

View File

@ -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 '<em class="placeholder">' + Drupal.checkPlain(str) + '</em>';
};
})(Drupal, window.drupalSettings, window.drupalTranslations);
})(Drupal, window.drupalSettings, window.drupalTranslations, window.console, window.Proxy, window.Reflect);

View File

@ -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 => {
const warnings = JSON.parse(
sessionStorage.getItem('js_deprecation_log_test.warnings') ||
JSON.stringify([]),
);
warnings.push(warning);
sessionStorage.setItem(
'js_deprecation_log_test.warnings',
JSON.stringify(warnings),
);
originalWarnFunction(warning);
};
}
})();

View File

@ -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(sessionStorage.getItem('js_deprecation_log_test.warnings') || JSON.stringify([]));
warnings.push(warning);
sessionStorage.setItem('js_deprecation_log_test.warnings', JSON.stringify(warnings));
originalWarnFunction(warning);
};
}
})();

View File

@ -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

View File

@ -0,0 +1,6 @@
deprecation_log:
version: VERSION
js:
js/js_deprecation_log.js: { weight: -100 }
dependencies:
- core/drupal

View File

@ -0,0 +1,21 @@
<?php
/**
* @file
* Helper module for the JavaScript deprecation tests.
*/
/**
* Implements hook_page_attachments().
*/
function js_deprecation_log_test_page_attachments(array &$attachments) {
// Unconditionally attach an asset to the page.
$attachments['#attached']['library'][] = 'js_deprecation_log_test/deprecation_log';
}
/**
* Implements hook_js_settings_alter().
*/
function js_deprecation_log_test_js_settings_alter(&$settings) {
$settings['suppressDeprecationErrors'] = FALSE;
}

View File

@ -0,0 +1,24 @@
/**
* @file
* Testing tools for deprecating JavaScript functions and class properties.
*/
(function({ deprecationError, deprecatedProperty, behaviors }) {
const deprecatedFunction = () => {
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);

View File

@ -0,0 +1,30 @@
/**
* 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);

View File

@ -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

View File

@ -0,0 +1,6 @@
deprecation_test:
version: VERSION
js:
js/js_deprecation_test.js: {}
dependencies:
- core/drupal

View File

@ -0,0 +1,13 @@
<?php
/**
* @file
* Helper module for the JavaScript deprecation tests.
*/
/**
* Implements hook_js_settings_alter().
*/
function js_deprecation_test_js_settings_alter(&$settings) {
$settings['suppressDeprecationErrors'] = FALSE;
}

View File

@ -0,0 +1,7 @@
js_deprecation_test.deprecation:
path: '/js_deprecation_test'
defaults:
_controller: '\Drupal\js_deprecation_test\Controller\JsDeprecationTestController::jsDeprecationTest'
_title: 'JsDeprecationTest'
requirements:
_access: 'TRUE'

View File

@ -0,0 +1,22 @@
<?php
namespace Drupal\js_deprecation_test\Controller;
/**
* Test Controller to show message links.
*/
class JsDeprecationTestController {
/**
* Renders page that has js_deprecation_test/deprecation library attached.
*
* @return array
* Render array.
*/
public function jsDeprecationTest() {
return [
'#attached' => ['library' => ['js_deprecation_test/deprecation_test']],
];
}
}

View File

@ -0,0 +1,12 @@
cache_strings: true
javascript:
directory: languages
translation:
use_source: remote_and_local
default_filename: '%project-%version.%language.po'
default_server_pattern: 'http://ftp.drupal.org/files/translations/%core/%project/%project-%version.%language.po'
overwrite_customized: false
overwrite_not_customized: true
update_interval_days: 0
path: ''
import_enabled: false

View File

@ -0,0 +1,8 @@
name: Nightwatch Testing
type: profile
description: 'Minimal profile for running Nightwatch tests. Includes absolutely required modules only.'
version: VERSION
core: 8.x
hidden: true
install:
- js_deprecation_log_test

View File

@ -0,0 +1,26 @@
<?php
namespace Drupal\FunctionalJavascriptTests;
/**
* Tests Javascript deprecation notices.
*
* @group javascript
* @group legacy
*/
class JavascriptDeprecationTest extends WebDriverTestBase {
public static $modules = ['js_deprecation_test'];
/**
* @expectedDeprecation Javascript Deprecation: This function is deprecated for testing purposes.
* @expectedDeprecation Javascript Deprecation: This property is deprecated for testing purposes.
*/
public function testJavascriptDeprecation() {
$this->drupalGet('js_deprecation_test');
// Ensure that deprecation message from previous page loads will be
// detected.
$this->drupalGet('user');
}
}

View File

@ -76,8 +76,9 @@ abstract class WebDriverTestBase extends BrowserTestBase {
* {@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 @@ abstract class WebDriverTestBase extends BrowserTestBase {
// explaining what the problem is.
throw new \RuntimeException('Unfinished AJAX requests while tearing down a test');
}
$warnings = $this->getSession()->evaluateScript("JSON.parse(sessionStorage.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();
}

View File

@ -0,0 +1,23 @@
module.exports.assertion = function(expected) {
this.message = `Testing if "${expected}" deprecation error has been triggered`;
this.expected = expected;
this.pass = deprecationMessages => deprecationMessages.includes(expected);
this.value = result => {
const sessionStorageEntries = JSON.parse(result.value);
const deprecationMessages =
sessionStorageEntries !== null
? sessionStorageEntries.filter(message =>
new RegExp('[Deprecation]').test(message),
)
: [];
return deprecationMessages.map(message =>
message.replace('[Deprecation] ', ''),
);
};
this.command = callback =>
// eslint-disable-next-line prefer-arrow-callback
this.api.execute(function() {
return window.sessionStorage.getItem('js_deprecation_log_test.warnings');
}, callback);
};

View File

@ -0,0 +1,23 @@
module.exports.assertion = function() {
this.message = 'Ensuring no deprecation errors have been triggered';
this.expected = '';
this.pass = deprecationMessages => deprecationMessages.length === 0;
this.value = result => {
const sessionStorageEntries = JSON.parse(result.value);
const deprecationMessages =
sessionStorageEntries !== null
? sessionStorageEntries.filter(message =>
new RegExp('[Deprecation]').test(message),
)
: [];
return deprecationMessages.map(message =>
message.replace('[Deprecation] ', ''),
);
};
this.command = callback =>
// eslint-disable-next-line prefer-arrow-callback
this.api.execute(function() {
return window.sessionStorage.getItem('js_deprecation_log_test.warnings');
}, callback);
};

View File

@ -46,7 +46,7 @@ exports.command = function drupalCreateUser(
})
.submitForm('#user-register-form')
.assert.containsText(
'.messages',
'[data-drupal-messages]',
'Created a new user account',
`User "${name}" was created successfully.`,
);

View File

@ -25,7 +25,7 @@ exports.command = function drupalInstall({ setupFile = '' } = {}, callback) {
: '';
const install = execSync(
commandAsWebserver(
`php ./scripts/test-site.php install ${setupFile} --base-url ${process.env.DRUPAL_TEST_BASE_URL} ${dbOption} --json`,
`php ./scripts/test-site.php install ${setupFile} --install-profile nightwatch_testing --base-url ${process.env.DRUPAL_TEST_BASE_URL} ${dbOption} --json`,
),
);
const installData = JSON.parse(install.toString());

View File

@ -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 });
},
};

View File

@ -0,0 +1,32 @@
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', 'JsDeprecationTest')
.assert.deprecationErrorExists(
'This function is deprecated for testing purposes.',
)
.assert.deprecationErrorExists(
'This property is deprecated for testing purposes.',
)
.drupalLogAndEnd({ onlyOnError: false });
},
};

View File

@ -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', 'Reports')
.assert.noDeprecationErrors();
},
};

View File

@ -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();
},
};