diff --git a/composer.json b/composer.json index d9c5d7c..727e031 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "wikimedia/composer-merge-plugin": "~1.3" }, "replace": { - "drupal/core": "~8.1" + "drupal/core": "~8.2" }, "minimum-stability": "dev", "prefer-stable": true, diff --git a/composer.lock b/composer.lock index 8688121..e86554f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "5e14f057cfce03180014a6a75e38de6f", - "content-hash": "7d72dc63a3981fb6edb92f6786ba94b1", + "hash": "7d101b08e5ae002d827cd42ae9a4e344", + "content-hash": "60f7057617c6d995bf9946d0b12f0b5d", "packages": [ { "name": "composer/installers", @@ -1057,12 +1057,12 @@ "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + "reference": "1.0.0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "url": "https://api.github.com/repos/php-fig/log/zipball/1.0.0", + "reference": "1.0.0", "shasum": "" }, "type": "library", @@ -1091,22 +1091,22 @@ }, { "name": "stack/builder", - "version": "v1.0.3", + "version": "v1.0.4", "source": { "type": "git", "url": "https://github.com/stackphp/builder.git", - "reference": "c1f8a4693b55c563405024f708a76ef576c3b276" + "reference": "59fcc9b448a8ce5e338a04c4e2e4aca893e83425" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stackphp/builder/zipball/c1f8a4693b55c563405024f708a76ef576c3b276", - "reference": "c1f8a4693b55c563405024f708a76ef576c3b276", + "url": "https://api.github.com/repos/stackphp/builder/zipball/59fcc9b448a8ce5e338a04c4e2e4aca893e83425", + "reference": "59fcc9b448a8ce5e338a04c4e2e4aca893e83425", "shasum": "" }, "require": { "php": ">=5.3.0", - "symfony/http-foundation": "~2.1", - "symfony/http-kernel": "~2.1" + "symfony/http-foundation": "~2.1|~3.0", + "symfony/http-kernel": "~2.1|~3.0" }, "require-dev": { "silex/silex": "~1.0" @@ -1136,40 +1136,42 @@ "keywords": [ "stack" ], - "time": "2014-11-23 20:37:11" + "time": "2016-06-02 06:58:42" }, { "name": "symfony-cmf/routing", - "version": "1.3.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/symfony-cmf/Routing.git", - "reference": "8e87981d72c6930a27585dcd3119f3199f6cb2a6" + "reference": "b93704ca098334f56e9b317932f21a4362e620db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony-cmf/Routing/zipball/8e87981d72c6930a27585dcd3119f3199f6cb2a6", - "reference": "8e87981d72c6930a27585dcd3119f3199f6cb2a6", + "url": "https://api.github.com/repos/symfony-cmf/Routing/zipball/b93704ca098334f56e9b317932f21a4362e620db", + "reference": "b93704ca098334f56e9b317932f21a4362e620db", "shasum": "" }, "require": { - "php": ">=5.3.3", - "psr/log": "~1.0", - "symfony/http-kernel": "~2.2", - "symfony/routing": "~2.2" + "php": "^5.3.9|^7.0", + "psr/log": "1.*", + "symfony/http-kernel": "^2.2|3.*", + "symfony/routing": "^2.2|3.*" }, "require-dev": { - "symfony/config": "~2.2", - "symfony/dependency-injection": "~2.0@stable", - "symfony/event-dispatcher": "~2.1" + "friendsofsymfony/jsrouting-bundle": "^1.1", + "symfony-cmf/testing": "^1.3", + "symfony/config": "^2.2|3.*", + "symfony/dependency-injection": "^2.0.5|3.*", + "symfony/event-dispatcher": "^2.1|3.*" }, "suggest": { - "symfony/event-dispatcher": "DynamicRouter can optionally trigger an event at the start of matching. Minimal version ~2.1" + "symfony/event-dispatcher": "DynamicRouter can optionally trigger an event at the start of matching. Minimal version (~2.1)" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -1193,7 +1195,7 @@ "database", "routing" ], - "time": "2014-10-20 20:55:17" + "time": "2016-03-31 09:11:39" }, { "name": "symfony/class-loader", @@ -2335,16 +2337,16 @@ }, { "name": "twig/twig", - "version": "v1.23.1", + "version": "v1.24.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "d9b6333ae8dd2c8e3fd256e127548def0bc614c6" + "reference": "3e5aa30ebfbafd5951fb1b01e338e1800ce7e0e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/d9b6333ae8dd2c8e3fd256e127548def0bc614c6", - "reference": "d9b6333ae8dd2c8e3fd256e127548def0bc614c6", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3e5aa30ebfbafd5951fb1b01e338e1800ce7e0e8", + "reference": "3e5aa30ebfbafd5951fb1b01e338e1800ce7e0e8", "shasum": "" }, "require": { @@ -2357,7 +2359,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.23-dev" + "dev-master": "1.24-dev" } }, "autoload": { @@ -2392,7 +2394,7 @@ "keywords": [ "templating" ], - "time": "2015-11-05 12:49:06" + "time": "2016-01-25 21:22:18" }, { "name": "wikimedia/composer-merge-plugin", diff --git a/core/assets/vendor/jquery/jquery.js b/core/assets/vendor/jquery/jquery.js index eed1777..3854747 100644 --- a/core/assets/vendor/jquery/jquery.js +++ b/core/assets/vendor/jquery/jquery.js @@ -1,15 +1,15 @@ /*! - * jQuery JavaScript Library v2.1.4 + * jQuery JavaScript Library v2.2.3 * http://jquery.com/ * * Includes Sizzle.js * http://sizzlejs.com/ * - * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors + * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2015-04-28T16:01Z + * Date: 2016-04-05T19:26Z */ (function( global, factory ) { @@ -41,10 +41,11 @@ // Can't be in strict mode, several libs including ASP.NET trace // the stack via arguments.caller.callee and Firefox dies if // you try to trace through "use strict" call chains. (#13335) -// - +//"use strict"; var arr = []; +var document = window.document; + var slice = arr.slice; var concat = arr.concat; @@ -64,13 +65,11 @@ var support = {}; var - // Use the correct document accordingly with window argument (sandbox) - document = window.document, - - version = "2.1.4", + version = "2.2.3", // Define a local copy of jQuery jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); @@ -90,6 +89,7 @@ var }; jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used jquery: version, @@ -133,16 +133,14 @@ jQuery.fn = jQuery.prototype = { }, // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); + each: function( callback ) { + return jQuery.each( this, callback ); }, map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { return callback.call( elem, i, elem ); - })); + } ) ); }, slice: function() { @@ -160,11 +158,11 @@ jQuery.fn = jQuery.prototype = { eq: function( i ) { var len = this.length, j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); }, end: function() { - return this.prevObject || this.constructor(null); + return this.prevObject || this.constructor(); }, // For internal use only. @@ -176,7 +174,7 @@ jQuery.fn = jQuery.prototype = { jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, - target = arguments[0] || {}, + target = arguments[ 0 ] || {}, i = 1, length = arguments.length, deep = false; @@ -191,7 +189,7 @@ jQuery.extend = jQuery.fn.extend = function() { } // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } @@ -202,8 +200,10 @@ jQuery.extend = jQuery.fn.extend = function() { } for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { + if ( ( options = arguments[ i ] ) != null ) { + // Extend the base object for ( name in options ) { src = target[ name ]; @@ -215,13 +215,15 @@ jQuery.extend = jQuery.fn.extend = function() { } // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + if ( copyIsArray ) { copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; + clone = src && jQuery.isArray( src ) ? src : []; } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; + clone = src && jQuery.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them @@ -239,7 +241,8 @@ jQuery.extend = jQuery.fn.extend = function() { return target; }; -jQuery.extend({ +jQuery.extend( { + // Unique for each copy of jQuery on the page expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), @@ -253,7 +256,7 @@ jQuery.extend({ noop: function() {}, isFunction: function( obj ) { - return jQuery.type(obj) === "function"; + return jQuery.type( obj ) === "function"; }, isArray: Array.isArray, @@ -263,14 +266,18 @@ jQuery.extend({ }, isNumeric: function( obj ) { + // parseFloat NaNs numeric-cast false positives (null|true|false|"") // ...but misinterprets leading-number strings, particularly hex literals ("0x...") // subtraction forces infinities to NaN // adding 1 corrects loss of precision from parseFloat (#15100) - return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0; + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; }, isPlainObject: function( obj ) { + var key; + // Not plain objects: // - Any object or value whose internal [[Class]] property is not "[object Object]" // - DOM nodes @@ -279,14 +286,18 @@ jQuery.extend({ return false; } + // Not own constructor property must be Object if ( obj.constructor && - !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { + !hasOwn.call( obj, "constructor" ) && + !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) { return false; } - // If the function hasn't returned already, we're confident that - // |obj| is a plain object, created by {} or constructed with new Object - return true; + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); }, isEmptyObject: function( obj ) { @@ -301,9 +312,10 @@ jQuery.extend({ if ( obj == null ) { return obj + ""; } + // Support: Android<4.0, iOS<6 (functionish RegExp) return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call(obj) ] || "object" : + class2type[ toString.call( obj ) ] || "object" : typeof obj; }, @@ -315,16 +327,19 @@ jQuery.extend({ code = jQuery.trim( code ); if ( code ) { + // If the code includes a valid, prologue position // strict mode pragma, execute code by injecting a // script tag into the document. - if ( code.indexOf("use strict") === 1 ) { - script = document.createElement("script"); + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); script.text = code; document.head.appendChild( script ).parentNode.removeChild( script ); } else { - // Otherwise, avoid the DOM node creation, insertion - // and removal by using an indirect global eval + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + indirect( code ); } } @@ -341,49 +356,20 @@ jQuery.extend({ return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); }, - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); + each: function( obj, callback ) { + var length, i = 0; - if ( value === false ) { - break; - } + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; } } - - // A special, fast, case for the most common use of each } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; } } } @@ -403,7 +389,7 @@ jQuery.extend({ var ret = results || []; if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { + if ( isArrayLike( Object( arr ) ) ) { jQuery.merge( ret, typeof arr === "string" ? [ arr ] : arr @@ -455,14 +441,13 @@ jQuery.extend({ // arg is for internal usage only map: function( elems, callback, arg ) { - var value, + var length, value, i = 0, - length = elems.length, - isArray = isArraylike( elems ), ret = []; // Go through the array, translating each of the items to their new values - if ( isArray ) { + if ( isArrayLike( elems ) ) { + length = elems.length; for ( ; i < length; i++ ) { value = callback( elems[ i ], i, arg ); @@ -523,43 +508,50 @@ jQuery.extend({ // jQuery.support is not used in Core but other projects attach their // properties to it so it needs to exist. support: support -}); +} ); + +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ // Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); +} ); -function isArraylike( obj ) { +function isArrayLike( obj ) { // Support: iOS 8.2 (not reproducible in simulator) // `in` check used to prevent JIT error (gh-2145) // hasOwn isn't used here due to false negatives // regarding Nodelist length in IE - var length = "length" in obj && obj.length, + var length = !!obj && "length" in obj && obj.length, type = jQuery.type( obj ); if ( type === "function" || jQuery.isWindow( obj ) ) { return false; } - if ( obj.nodeType === 1 && length ) { - return true; - } - return type === "array" || length === 0 || typeof length === "number" && length > 0 && ( length - 1 ) in obj; } var Sizzle = /*! - * Sizzle CSS Selector Engine v2.2.0-pre + * Sizzle CSS Selector Engine v2.2.1 * http://sizzlejs.com/ * - * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors + * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2014-12-16 + * Date: 2015-10-17 */ (function( window ) { @@ -627,25 +619,21 @@ var i, // Regular expressions - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + // http://www.w3.org/TR/css3-selectors/#whitespace whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + // Operator (capture 2) "*([*^$|!~]?=)" + whitespace + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + "*\\]", - pseudos = ":(" + characterEncoding + ")(?:\\((" + + pseudos = ":(" + identifier + ")(?:\\((" + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: // 1. quoted (capture 3; capture 4 or capture 5) "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + @@ -668,9 +656,9 @@ var i, ridentifier = new RegExp( "^" + identifier + "$" ), matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), "ATTR": new RegExp( "^" + attributes ), "PSEUDO": new RegExp( "^" + pseudos ), "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + @@ -748,103 +736,129 @@ try { } function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; - context = context || document; results = results || []; - nodeType = context.nodeType; + // Return early from calls with invalid selector or context if ( typeof selector !== "string" || !selector || nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { return results; } - if ( !seed && documentIsHTML ) { + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { - // Try to shortcut find operations when possible (e.g., not under DocumentFragment) - if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document (jQuery #6963) - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { return results; } + + // Element context } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } } - } - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } } - } - // QSA path - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType !== 1 && selector; + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; } - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; - newSelector = groups.join(","); - } - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } } } } @@ -857,7 +871,7 @@ function Sizzle( selector, context, results, seed ) { /** * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * @returns {function(string, object)} Returns the Object data after storing it on itself with * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) * deleting the oldest entry */ @@ -912,7 +926,7 @@ function assert( fn ) { */ function addHandle( attrs, handler ) { var arr = attrs.split("|"), - i = attrs.length; + i = arr.length; while ( i-- ) { Expr.attrHandle[ arr[i] ] = handler; @@ -1025,33 +1039,29 @@ setDocument = Sizzle.setDocument = function( node ) { var hasCompare, parent, doc = node ? node.ownerDocument || node : preferredDoc; - // If no document and documentElement is available, return + // Return early if doc is invalid or already selected if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { return document; } - // Set our document + // Update global variables document = doc; - docElem = doc.documentElement; - parent = doc.defaultView; - - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent !== parent.top ) { - // IE11 does not have attachEvent, so all must suffer + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 if ( parent.addEventListener ) { parent.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only } else if ( parent.attachEvent ) { parent.attachEvent( "onunload", unloadHandler ); } } - /* Support tests - ---------------------------------------------------------------------- */ - documentIsHTML = !isXML( doc ); - /* Attributes ---------------------------------------------------------------------- */ @@ -1068,12 +1078,12 @@ setDocument = Sizzle.setDocument = function( node ) { // Check if getElementsByTagName("*") returns only elements support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); + div.appendChild( document.createComment("") ); return !div.getElementsByTagName("*").length; }); // Support: IE<9 - support.getElementsByClassName = rnative.test( doc.getElementsByClassName ); + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); // Support: IE<10 // Check if getElementById returns elements by name @@ -1081,7 +1091,7 @@ setDocument = Sizzle.setDocument = function( node ) { // so use a roundabout getElementsByName test support.getById = assert(function( div ) { docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + return !document.getElementsByName || !document.getElementsByName( expando ).length; }); // ID find and filter @@ -1089,9 +1099,7 @@ setDocument = Sizzle.setDocument = function( node ) { Expr.find["ID"] = function( id, context ) { if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [ m ] : []; + return m ? [ m ] : []; } }; Expr.filter["ID"] = function( id ) { @@ -1108,7 +1116,8 @@ setDocument = Sizzle.setDocument = function( node ) { Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); return node && node.value === attrId; }; }; @@ -1148,7 +1157,7 @@ setDocument = Sizzle.setDocument = function( node ) { // Class Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( documentIsHTML ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { return context.getElementsByClassName( className ); } }; @@ -1168,7 +1177,7 @@ setDocument = Sizzle.setDocument = function( node ) { // See http://bugs.jquery.com/ticket/13378 rbuggyQSA = []; - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { // Build QSA regex // Regex strategy adopted from Diego Perini assert(function( div ) { @@ -1178,7 +1187,7 @@ setDocument = Sizzle.setDocument = function( node ) { // since its presence should be enough // http://bugs.jquery.com/ticket/12359 docElem.appendChild( div ).innerHTML = "" + - "" + + "" + ""; // Support: IE8, Opera 11-12.16 @@ -1195,7 +1204,7 @@ setDocument = Sizzle.setDocument = function( node ) { rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); } - // Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+ + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { rbuggyQSA.push("~="); } @@ -1218,7 +1227,7 @@ setDocument = Sizzle.setDocument = function( node ) { assert(function( div ) { // Support: Windows 8 Native Apps // The type and name attributes are restricted during .innerHTML assignment - var input = doc.createElement("input"); + var input = document.createElement("input"); input.setAttribute( "type", "hidden" ); div.appendChild( input ).setAttribute( "name", "D" ); @@ -1266,7 +1275,7 @@ setDocument = Sizzle.setDocument = function( node ) { hasCompare = rnative.test( docElem.compareDocumentPosition ); // Element contains another - // Purposefully does not implement inclusive descendent + // Purposefully self-exclusive // As in, an element does not contain itself contains = hasCompare || rnative.test( docElem.contains ) ? function( a, b ) { @@ -1320,10 +1329,10 @@ setDocument = Sizzle.setDocument = function( node ) { (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { // Choose the first element that is related to our preferred document - if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { return -1; } - if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { return 1; } @@ -1351,8 +1360,8 @@ setDocument = Sizzle.setDocument = function( node ) { // Parentless nodes are either documents or disconnected if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : + return a === document ? -1 : + b === document ? 1 : aup ? -1 : bup ? 1 : sortInput ? @@ -1389,7 +1398,7 @@ setDocument = Sizzle.setDocument = function( node ) { 0; }; - return doc; + return document; }; Sizzle.matches = function( expr, elements ) { @@ -1406,6 +1415,7 @@ Sizzle.matchesSelector = function( elem, expr ) { expr = expr.replace( rattributeQuotes, "='$1']" ); if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { @@ -1679,11 +1689,12 @@ Expr = Sizzle.selectors = { } : function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, + var cache, uniqueCache, outerCache, node, nodeIndex, start, dir = simple !== forward ? "nextSibling" : "previousSibling", parent = elem.parentNode, name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; + useCache = !xml && !ofType, + diff = false; if ( parent ) { @@ -1692,7 +1703,10 @@ Expr = Sizzle.selectors = { while ( dir ) { node = elem; while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + return false; } } @@ -1706,11 +1720,21 @@ Expr = Sizzle.selectors = { // non-xml :nth-child(...) stores cache data on `parent` if ( forward && useCache ) { + // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; node = nodeIndex && parent.childNodes[ nodeIndex ]; while ( (node = ++nodeIndex && node && node[ dir ] || @@ -1720,29 +1744,55 @@ Expr = Sizzle.selectors = { // When found, cache indexes on `parent` and break if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; break; } } - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); - if ( node === elem ) { - break; + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } } } } @@ -2104,10 +2154,10 @@ function addCombinator( matcher, combinator, base ) { // Check against all ancestor/preceding elements function( elem, context, xml ) { - var oldCache, outerCache, + var oldCache, uniqueCache, outerCache, newCache = [ dirruns, doneName ]; - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching if ( xml ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { @@ -2120,14 +2170,19 @@ function addCombinator( matcher, combinator, base ) { while ( (elem = elem[ dir ]) ) { if ( elem.nodeType === 1 || checkNonElements ) { outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (oldCache = outerCache[ dir ]) && + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { // Assign to newCache so results back-propagate to previous elements return (newCache[ 2 ] = oldCache[ 2 ]); } else { // Reuse newcache so results back-propagate to previous elements - outerCache[ dir ] = newCache; + uniqueCache[ dir ] = newCache; // A match means we're done; a fail means we have to keep checking if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { @@ -2352,18 +2407,21 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { len = elems.length; if ( outermost ) { - outermostContext = context !== document && context; + outermostContext = context === document || context || outermost; } // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below // Support: IE<9, Safari // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id for ( ; i !== len && (elem = elems[i]) != null; i++ ) { if ( byElement && elem ) { j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { + if ( matcher( elem, context || document, xml) ) { results.push( elem ); break; } @@ -2387,8 +2445,17 @@ function matcherFromGroupMatchers( elementMatchers, setMatchers ) { } } - // Apply set filters to unmatched elements + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. if ( bySet && i !== matchedCount ) { j = 0; while ( (matcher = setMatchers[j++]) ) { @@ -2480,10 +2547,11 @@ select = Sizzle.select = function( selector, context, results, seed ) { results = results || []; - // Try to minimize operations if there is no seed and only one group + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) if ( match.length === 1 ) { - // Take a shortcut and set the context if the root selector is an ID + // Reduce context if the leading compound selector is an ID tokens = match[0] = match[0].slice( 0 ); if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && support.getById && context.nodeType === 9 && documentIsHTML && @@ -2538,7 +2606,7 @@ select = Sizzle.select = function( selector, context, results, seed ) { context, !documentIsHTML, results, - rsibling.test( selector ) && testContext( context.parentNode ) || context + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context ); return results; }; @@ -2614,17 +2682,46 @@ return Sizzle; jQuery.find = Sizzle; jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; jQuery.text = Sizzle.getText; jQuery.isXMLDoc = Sizzle.isXML; jQuery.contains = Sizzle.contains; +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + var rneedsContext = jQuery.expr.match.needsContext; -var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); @@ -2636,14 +2733,14 @@ function winnow( elements, qualifier, not ) { return jQuery.grep( elements, function( elem, i ) { /* jshint -W018 */ return !!qualifier.call( elem, i, elem ) !== not; - }); + } ); } if ( qualifier.nodeType ) { return jQuery.grep( elements, function( elem ) { return ( elem === qualifier ) !== not; - }); + } ); } @@ -2656,8 +2753,8 @@ function winnow( elements, qualifier, not ) { } return jQuery.grep( elements, function( elem ) { - return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; - }); + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); } jQuery.filter = function( expr, elems, not ) { @@ -2671,10 +2768,10 @@ jQuery.filter = function( expr, elems, not ) { jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { return elem.nodeType === 1; - })); + } ) ); }; -jQuery.fn.extend({ +jQuery.fn.extend( { find: function( selector ) { var i, len = this.length, @@ -2682,13 +2779,13 @@ jQuery.fn.extend({ self = this; if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter(function() { + return this.pushStack( jQuery( selector ).filter( function() { for ( i = 0; i < len; i++ ) { if ( jQuery.contains( self[ i ], this ) ) { return true; } } - }) ); + } ) ); } for ( i = 0; i < len; i++ ) { @@ -2701,10 +2798,10 @@ jQuery.fn.extend({ return ret; }, filter: function( selector ) { - return this.pushStack( winnow(this, selector || [], false) ); + return this.pushStack( winnow( this, selector || [], false ) ); }, not: function( selector ) { - return this.pushStack( winnow(this, selector || [], true) ); + return this.pushStack( winnow( this, selector || [], true ) ); }, is: function( selector ) { return !!winnow( @@ -2718,7 +2815,7 @@ jQuery.fn.extend({ false ).length; } -}); +} ); // Initialize a jQuery object @@ -2732,7 +2829,7 @@ var rootjQuery, // Strict HTML recognition (#11290: must start with <) rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - init = jQuery.fn.init = function( selector, context ) { + init = jQuery.fn.init = function( selector, context, root ) { var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false) @@ -2740,9 +2837,16 @@ var rootjQuery, return this; } + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + // Handle HTML strings if ( typeof selector === "string" ) { - if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; @@ -2751,23 +2855,24 @@ var rootjQuery, } // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { + if ( match && ( match[ 1 ] || !context ) ) { // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; // Option to run scripts is true for back-compat // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( - match[1], + match[ 1 ], context && context.nodeType ? context.ownerDocument || context : document, true ) ); // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { for ( match in context ) { + // Properties of context are called as methods if possible if ( jQuery.isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); @@ -2783,14 +2888,15 @@ var rootjQuery, // HANDLE: $(#id) } else { - elem = document.getElementById( match[2] ); + elem = document.getElementById( match[ 2 ] ); // Support: Blackberry 4.6 // gEBID returns nodes no longer in the document (#6963) if ( elem && elem.parentNode ) { + // Inject the element directly into the jQuery object this.length = 1; - this[0] = elem; + this[ 0 ] = elem; } this.context = document; @@ -2800,7 +2906,7 @@ var rootjQuery, // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); + return ( context || root ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) @@ -2810,15 +2916,16 @@ var rootjQuery, // HANDLE: $(DOMElement) } else if ( selector.nodeType ) { - this.context = this[0] = selector; + this.context = this[ 0 ] = selector; this.length = 1; return this; // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { - return typeof rootjQuery.ready !== "undefined" ? - rootjQuery.ready( selector ) : + return root.ready !== undefined ? + root.ready( selector ) : + // Execute immediately if ready is not present selector( jQuery ); } @@ -2839,6 +2946,7 @@ rootjQuery = jQuery( document ); var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // Methods guaranteed to produce a unique set when starting from a unique set guaranteedUnique = { children: true, @@ -2847,48 +2955,19 @@ var rparentsprev = /^(?:parents|prev(?:Until|All))/, prev: true }; -jQuery.extend({ - dir: function( elem, dir, until ) { - var matched = [], - truncate = until !== undefined; - - while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { - if ( elem.nodeType === 1 ) { - if ( truncate && jQuery( elem ).is( until ) ) { - break; - } - matched.push( elem ); - } - } - return matched; - }, - - sibling: function( n, elem ) { - var matched = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - matched.push( n ); - } - } - - return matched; - } -}); - -jQuery.fn.extend({ +jQuery.fn.extend( { has: function( target ) { var targets = jQuery( target, this ), l = targets.length; - return this.filter(function() { + return this.filter( function() { var i = 0; for ( ; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { + if ( jQuery.contains( this, targets[ i ] ) ) { return true; } } - }); + } ); }, closest: function( selectors, context ) { @@ -2901,14 +2980,15 @@ jQuery.fn.extend({ 0; for ( ; i < l; i++ ) { - for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments - if ( cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : // Don't pass non-elements to Sizzle cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors)) ) { + jQuery.find.matchesSelector( cur, selectors ) ) ) { matched.push( cur ); break; @@ -2916,7 +2996,7 @@ jQuery.fn.extend({ } } - return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); }, // Determine the position of an element within the set @@ -2942,7 +3022,7 @@ jQuery.fn.extend({ add: function( selector, context ) { return this.pushStack( - jQuery.unique( + jQuery.uniqueSort( jQuery.merge( this.get(), jQuery( selector, context ) ) ) ); @@ -2950,26 +3030,26 @@ jQuery.fn.extend({ addBack: function( selector ) { return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) + this.prevObject : this.prevObject.filter( selector ) ); } -}); +} ); function sibling( cur, dir ) { - while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} return cur; } -jQuery.each({ +jQuery.each( { parent: function( elem ) { var parent = elem.parentNode; return parent && parent.nodeType !== 11 ? parent : null; }, parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); + return dir( elem, "parentNode" ); }, parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); + return dir( elem, "parentNode", until ); }, next: function( elem ) { return sibling( elem, "nextSibling" ); @@ -2978,22 +3058,22 @@ jQuery.each({ return sibling( elem, "previousSibling" ); }, nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); + return dir( elem, "nextSibling" ); }, prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); + return dir( elem, "previousSibling" ); }, nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); + return dir( elem, "nextSibling", until ); }, prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); + return dir( elem, "previousSibling", until ); }, siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + return siblings( ( elem.parentNode || {} ).firstChild, elem ); }, children: function( elem ) { - return jQuery.sibling( elem.firstChild ); + return siblings( elem.firstChild ); }, contents: function( elem ) { return elem.contentDocument || jQuery.merge( [], elem.childNodes ); @@ -3011,9 +3091,10 @@ jQuery.each({ } if ( this.length > 1 ) { + // Remove duplicates if ( !guaranteedUnique[ name ] ) { - jQuery.unique( matched ); + jQuery.uniqueSort( matched ); } // Reverse order for parents* and prev-derivatives @@ -3024,20 +3105,17 @@ jQuery.each({ return this.pushStack( matched ); }; -}); -var rnotwhite = (/\S+/g); +} ); +var rnotwhite = ( /\S+/g ); -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache +// Convert String-formatted options into Object-formatted ones function createOptions( options ) { - var object = optionsCache[ options ] = {}; + var object = {}; jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { object[ flag ] = true; - }); + } ); return object; } @@ -3068,156 +3146,186 @@ jQuery.Callbacks = function( options ) { // Convert options from String-formatted to Object-formatted if needed // (we check in cache first) options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : + createOptions( options ) : jQuery.extend( {}, options ); - var // Last fire value (for non-forgettable lists) + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists memory, + // Flag to know if list was already fired fired, - // Flag to know if list is currently firing - firing, - // First callback to fire (used internally by add and fireWith) - firingStart, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, + + // Flag to prevent firing + locked, + // Actual callback list list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } } } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { list = []; + + // Otherwise, this object is spent } else { - self.disable(); + list = ""; } } }, + // Actual Callbacks object self = { + // Add a callback or a collection of callbacks to the list add: function() { if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { + if ( jQuery.isFunction( arg ) ) { if ( !options.unique || !self.has( arg ) ) { list.push( arg ); } - } else if ( arg && arg.length && type !== "string" ) { + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + // Inspect recursively add( arg ); } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); } } return this; }, + // Remove a callback from the list remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; } - }); - } + } + } ); return this; }, + // Check if a given callback is in the list. // If no argument is given, return whether or not list has callbacks attached. has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; }, + // Remove all callbacks from the list empty: function() { - list = []; - firingLength = 0; + if ( list ) { + list = []; + } return this; }, - // Have the list do nothing anymore + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values disable: function() { - list = stack = memory = undefined; + locked = queue = []; + list = memory = ""; return this; }, - // Is it disabled? disabled: function() { return !list; }, - // Lock the list in its current state + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions lock: function() { - stack = undefined; + locked = queue = []; if ( !memory ) { - self.disable(); + list = memory = ""; } return this; }, - // Is it locked? locked: function() { - return !stack; + return !!locked; }, + // Call all callbacks with the given context and arguments fireWith: function( context, args ) { - if ( list && ( !fired || stack ) ) { + if ( !locked ) { args = args || []; args = [ context, args.slice ? args.slice() : args ]; - if ( firing ) { - stack.push( args ); - } else { - fire( args ); + queue.push( args ); + if ( !firing ) { + fire(); } } return this; }, + // Call all the callbacks with the given arguments fire: function() { self.fireWith( this, arguments ); return this; }, + // To know if the callbacks have already been called at least once fired: function() { return !!fired; @@ -3228,14 +3336,15 @@ jQuery.Callbacks = function( options ) { }; -jQuery.extend({ +jQuery.extend( { Deferred: function( func ) { var tuples = [ + // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] ], state = "pending", promise = { @@ -3248,25 +3357,30 @@ jQuery.extend({ }, then: function( /* fnDone, fnFail, fnProgress */ ) { var fns = arguments; - return jQuery.Deferred(function( newDefer ) { + return jQuery.Deferred( function( newDefer ) { jQuery.each( tuples, function( i, tuple ) { var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { + deferred[ tuple[ 1 ] ]( function() { var returned = fn && fn.apply( this, arguments ); if ( returned && jQuery.isFunction( returned.promise ) ) { returned.promise() + .progress( newDefer.notify ) .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); + .fail( newDefer.reject ); } else { - newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); } - }); - }); + } ); + } ); fns = null; - }).promise(); + } ).promise(); }, + // Get a promise for this deferred // If obj is provided, the promise aspect is added to the object promise: function( obj ) { @@ -3284,11 +3398,12 @@ jQuery.extend({ stateString = tuple[ 3 ]; // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; + promise[ tuple[ 1 ] ] = list.add; // Handle state if ( stateString ) { - list.add(function() { + list.add( function() { + // state = [ resolved | rejected ] state = stateString; @@ -3297,12 +3412,12 @@ jQuery.extend({ } // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); return this; }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); // Make the deferred a promise promise.promise( deferred ); @@ -3323,9 +3438,11 @@ jQuery.extend({ length = resolveValues.length, // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. deferred = remaining === 1 ? subordinate : jQuery.Deferred(), // Update function for both resolve and progress values @@ -3351,9 +3468,9 @@ jQuery.extend({ for ( ; i < length; i++ ) { if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); + .fail( deferred.reject ); } else { --remaining; } @@ -3367,20 +3484,22 @@ jQuery.extend({ return deferred.promise(); } -}); +} ); // The deferred used on DOM ready var readyList; jQuery.fn.ready = function( fn ) { + // Add the callback jQuery.ready.promise().done( fn ); return this; }; -jQuery.extend({ +jQuery.extend( { + // Is the DOM ready to be used? Set to true once it occurs. isReady: false, @@ -3422,14 +3541,14 @@ jQuery.extend({ jQuery( document ).off( "ready" ); } } -}); +} ); /** * The ready event handler and self cleanup method */ function completed() { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); jQuery.ready(); } @@ -3438,20 +3557,23 @@ jQuery.ready.promise = function( obj ) { readyList = jQuery.Deferred(); - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // We once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); + window.setTimeout( jQuery.ready ); } else { // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); + document.addEventListener( "DOMContentLoaded", completed ); // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); + window.addEventListener( "load", completed ); } } return readyList.promise( obj ); @@ -3465,7 +3587,7 @@ jQuery.ready.promise(); // Multifunctional method to get and set values of a collection // The value/s can optionally be executed if it's a function -var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { var i = 0, len = elems.length, bulk = key == null; @@ -3474,7 +3596,7 @@ var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGe if ( jQuery.type( key ) === "object" ) { chainable = true; for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + access( elems, fn, i, key[ i ], true, emptyGet, raw ); } // Sets one value @@ -3486,6 +3608,7 @@ var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGe } if ( bulk ) { + // Bulk operations run against the entire set if ( raw ) { fn.call( elems, value ); @@ -3502,7 +3625,11 @@ var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGe if ( fn ) { for ( ; i < len; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); } } } @@ -3513,14 +3640,10 @@ var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGe // Gets bulk ? fn.call( elems ) : - len ? fn( elems[0], key ) : emptyGet; + len ? fn( elems[ 0 ], key ) : emptyGet; }; +var acceptData = function( owner ) { - -/** - * Determines whether an object can have data - */ -jQuery.acceptData = function( owner ) { // Accepts only: // - Node // - Node.ELEMENT_NODE @@ -3532,66 +3655,79 @@ jQuery.acceptData = function( owner ) { }; -function Data() { - // Support: Android<4, - // Old WebKit does not have Object.preventExtensions/freeze method, - // return new empty object instead with no [[set]] accessor - Object.defineProperty( this.cache = {}, 0, { - get: function() { - return {}; - } - }); + +function Data() { this.expando = jQuery.expando + Data.uid++; } Data.uid = 1; -Data.accepts = jQuery.acceptData; Data.prototype = { - key: function( owner ) { + + register: function( owner, initial ) { + var value = initial || {}; + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { + // We can accept data for non-element nodes in modern browsers, // but we should not, see #8335. - // Always return the key for a frozen object. - if ( !Data.accepts( owner ) ) { - return 0; + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; } - var descriptor = {}, - // Check if the owner object already has a cache key - unlock = owner[ this.expando ]; + // Check if the owner object already has a cache + var value = owner[ this.expando ]; // If not, create one - if ( !unlock ) { - unlock = Data.uid++; - - // Secure it in a non-enumerable, non-writable property - try { - descriptor[ this.expando ] = { value: unlock }; - Object.defineProperties( owner, descriptor ); - - // Support: Android<4 - // Fallback to a less secure definition - } catch ( e ) { - descriptor[ this.expando ] = unlock; - jQuery.extend( owner, descriptor ); + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } } } - // Ensure the cache object - if ( !this.cache[ unlock ] ) { - this.cache[ unlock ] = {}; - } - - return unlock; + return value; }, set: function( owner, data, value ) { var prop, - // There may be an unlock assigned to this node, - // if there is no entry for this "owner", create one inline - // and set the unlock as though an owner entry had always existed - unlock = this.key( owner ), - cache = this.cache[ unlock ]; + cache = this.cache( owner ); // Handle: [ owner, key, value ] args if ( typeof data === "string" ) { @@ -3599,30 +3735,22 @@ Data.prototype = { // Handle: [ owner, { properties } ] args } else { - // Fresh assignments by object are shallow copied - if ( jQuery.isEmptyObject( cache ) ) { - jQuery.extend( this.cache[ unlock ], data ); - // Otherwise, copy the properties one-by-one to the cache object - } else { - for ( prop in data ) { - cache[ prop ] = data[ prop ]; - } + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; } } return cache; }, get: function( owner, key ) { - // Either a valid cache is found, or will be created. - // New caches will be created and the unlock returned, - // allowing direct access to the newly created - // empty data object. A valid owner object must be provided. - var cache = this.cache[ this.key( owner ) ]; - return key === undefined ? - cache : cache[ key ]; + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; }, access: function( owner, key, value ) { var stored; + // In cases where either: // // 1. No key was specified @@ -3635,15 +3763,15 @@ Data.prototype = { // 2. The data stored at the key // if ( key === undefined || - ((key && typeof key === "string") && value === undefined) ) { + ( ( key && typeof key === "string" ) && value === undefined ) ) { stored = this.get( owner, key ); return stored !== undefined ? - stored : this.get( owner, jQuery.camelCase(key) ); + stored : this.get( owner, jQuery.camelCase( key ) ); } - // [*]When the key is not a string, or both a key and value + // When the key is not a string, or both a key and value // are specified, set or extend (existing objects) with either: // // 1. An object of properties @@ -3657,15 +3785,20 @@ Data.prototype = { }, remove: function( owner, key ) { var i, name, camel, - unlock = this.key( owner ), - cache = this.cache[ unlock ]; + cache = owner[ this.expando ]; - if ( key === undefined ) { - this.cache[ unlock ] = {}; + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); } else { + // Support array or space separated string of keys if ( jQuery.isArray( key ) ) { + // If "name" is an array of keys... // When data is initially created, via ("key", "val") signature, // keys will be converted to camelCase. @@ -3675,10 +3808,12 @@ Data.prototype = { name = key.concat( key.map( jQuery.camelCase ) ); } else { camel = jQuery.camelCase( key ); + // Try the string as a key before any manipulation if ( key in cache ) { name = [ key, camel ]; } else { + // If a key with the spaces exists, use it. // Otherwise, create an array by matching non-whitespace name = camel; @@ -3688,25 +3823,34 @@ Data.prototype = { } i = name.length; + while ( i-- ) { delete cache[ name[ i ] ]; } } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } }, hasData: function( owner ) { - return !jQuery.isEmptyObject( - this.cache[ owner[ this.expando ] ] || {} - ); - }, - discard: function( owner ) { - if ( owner[ this.expando ] ) { - delete this.cache[ owner[ this.expando ] ]; - } + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); } }; -var data_priv = new Data(); +var dataPriv = new Data(); -var data_user = new Data(); +var dataUser = new Data(); @@ -3721,7 +3865,7 @@ var data_user = new Data(); // 6. Provide a clear path for implementation upgrade to WeakMap in 2014 var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /([A-Z])/g; + rmultiDash = /[A-Z]/g; function dataAttr( elem, key, data ) { var name; @@ -3729,7 +3873,7 @@ function dataAttr( elem, key, data ) { // If nothing was found internally, try to fetch any // data from the HTML5 data-* attribute if ( data === undefined && elem.nodeType === 1 ) { - name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); data = elem.getAttribute( name ); if ( typeof data === "string" ) { @@ -3737,14 +3881,15 @@ function dataAttr( elem, key, data ) { data = data === "true" ? true : data === "false" ? false : data === "null" ? null : + // Only convert to a number if it doesn't change the string +data + "" === data ? +data : rbrace.test( data ) ? jQuery.parseJSON( data ) : data; - } catch( e ) {} + } catch ( e ) {} // Make sure we set the data so it isn't changed later - data_user.set( elem, key, data ); + dataUser.set( elem, key, data ); } else { data = undefined; } @@ -3752,31 +3897,31 @@ function dataAttr( elem, key, data ) { return data; } -jQuery.extend({ +jQuery.extend( { hasData: function( elem ) { - return data_user.hasData( elem ) || data_priv.hasData( elem ); + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); }, data: function( elem, name, data ) { - return data_user.access( elem, name, data ); + return dataUser.access( elem, name, data ); }, removeData: function( elem, name ) { - data_user.remove( elem, name ); + dataUser.remove( elem, name ); }, // TODO: Now that all calls to _data and _removeData have been replaced - // with direct calls to data_priv methods, these can be deprecated. + // with direct calls to dataPriv methods, these can be deprecated. _data: function( elem, name, data ) { - return data_priv.access( elem, name, data ); + return dataPriv.access( elem, name, data ); }, _removeData: function( elem, name ) { - data_priv.remove( elem, name ); + dataPriv.remove( elem, name ); } -}); +} ); -jQuery.fn.extend({ +jQuery.fn.extend( { data: function( key, value ) { var i, name, data, elem = this[ 0 ], @@ -3785,9 +3930,9 @@ jQuery.fn.extend({ // Gets all values if ( key === undefined ) { if ( this.length ) { - data = data_user.get( elem ); + data = dataUser.get( elem ); - if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { i = attrs.length; while ( i-- ) { @@ -3796,12 +3941,12 @@ jQuery.fn.extend({ if ( attrs[ i ] ) { name = attrs[ i ].name; if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice(5) ); + name = jQuery.camelCase( name.slice( 5 ) ); dataAttr( elem, name, data[ name ] ); } } } - data_priv.set( elem, "hasDataAttrs", true ); + dataPriv.set( elem, "hasDataAttrs", true ); } } @@ -3810,14 +3955,13 @@ jQuery.fn.extend({ // Sets multiple values if ( typeof key === "object" ) { - return this.each(function() { - data_user.set( this, key ); - }); + return this.each( function() { + dataUser.set( this, key ); + } ); } return access( this, function( value ) { - var data, - camelKey = jQuery.camelCase( key ); + var data, camelKey; // The calling jQuery object (element matches) is not empty // (and therefore has an element appears at this[ 0 ]) and the @@ -3825,16 +3969,24 @@ jQuery.fn.extend({ // will result in `undefined` for elem = this[ 0 ] which will // throw an exception if an attempt to read a data cache is made. if ( elem && value === undefined ) { + // Attempt to get data from the cache // with the key as-is - data = data_user.get( elem, key ); + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + if ( data !== undefined ) { return data; } + camelKey = jQuery.camelCase( key ); + // Attempt to get data from the cache // with the key camelized - data = data_user.get( elem, camelKey ); + data = dataUser.get( elem, camelKey ); if ( data !== undefined ) { return data; } @@ -3851,46 +4003,48 @@ jQuery.fn.extend({ } // Set the data... - this.each(function() { + camelKey = jQuery.camelCase( key ); + this.each( function() { + // First, attempt to store a copy or reference of any // data that might've been store with a camelCased key. - var data = data_user.get( this, camelKey ); + var data = dataUser.get( this, camelKey ); // For HTML5 data-* attribute interop, we have to // store property names with dashes in a camelCase form. // This might not apply to all properties...* - data_user.set( this, camelKey, value ); + dataUser.set( this, camelKey, value ); // *... In the case of properties that might _actually_ // have dashes, we need to also store a copy of that // unchanged property. - if ( key.indexOf("-") !== -1 && data !== undefined ) { - data_user.set( this, key, value ); + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); } - }); + } ); }, null, value, arguments.length > 1, null, true ); }, removeData: function( key ) { - return this.each(function() { - data_user.remove( this, key ); - }); + return this.each( function() { + dataUser.remove( this, key ); + } ); } -}); +} ); -jQuery.extend({ +jQuery.extend( { queue: function( elem, type, data ) { var queue; if ( elem ) { type = ( type || "fx" ) + "queue"; - queue = data_priv.get( elem, type ); + queue = dataPriv.get( elem, type ); // Speed up dequeue by getting out quickly if this is just a lookup if ( data ) { if ( !queue || jQuery.isArray( data ) ) { - queue = data_priv.access( elem, type, jQuery.makeArray(data) ); + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); } else { queue.push( data ); } @@ -3937,15 +4091,15 @@ jQuery.extend({ // Not public - generate a queueHooks object, or return the current one _queueHooks: function( elem, type ) { var key = type + "queueHooks"; - return data_priv.get( elem, key ) || data_priv.access( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - data_priv.remove( elem, [ type + "queue", key ] ); - }) - }); + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); } -}); +} ); -jQuery.fn.extend({ +jQuery.fn.extend( { queue: function( type, data ) { var setter = 2; @@ -3956,30 +4110,31 @@ jQuery.fn.extend({ } if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); + return jQuery.queue( this[ 0 ], type ); } return data === undefined ? this : - this.each(function() { + this.each( function() { var queue = jQuery.queue( this, type, data ); // Ensure a hooks for this queue jQuery._queueHooks( this, type ); - if ( type === "fx" && queue[0] !== "inprogress" ) { + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { jQuery.dequeue( this, type ); } - }); + } ); }, dequeue: function( type ) { - return this.each(function() { + return this.each( function() { jQuery.dequeue( this, type ); - }); + } ); }, clearQueue: function( type ) { return this.queue( type || "fx", [] ); }, + // Get a promise resolved when queues of a certain type // are emptied (fx is the type by default) promise: function( type, obj ) { @@ -4001,7 +4156,7 @@ jQuery.fn.extend({ type = type || "fx"; while ( i-- ) { - tmp = data_priv.get( elements[ i ], type + "queueHooks" ); + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); if ( tmp && tmp.empty ) { count++; tmp.empty.add( resolve ); @@ -4010,28 +4165,243 @@ jQuery.fn.extend({ resolve(); return defer.promise( obj ); } -}); -var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; // in that case, element will be second argument elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); }; -var rcheckableType = (/^(?:checkbox|radio)$/i); +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "", "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "" ], + col: [ 2, "", "" ], + tr: [ 2, "", "" ], + td: [ 3, "", "" ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + -(function() { +( function() { var fragment = document.createDocumentFragment(), div = fragment.appendChild( document.createElement( "div" ) ), input = document.createElement( "input" ); - // Support: Safari<=5.1 + // Support: Android 4.0-4.3, Safari<=5.1 // Check state lost if the name is set (#11217) // Support: Windows Web Apps (WWA) // `name` and `type` must use .setAttribute for WWA (#14901) @@ -4049,19 +4419,13 @@ var rcheckableType = (/^(?:checkbox|radio)$/i); // Make sure textarea (and checkbox) defaultValue is properly cloned div.innerHTML = "x"; support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; -})(); -var strundefined = typeof undefined; - - - -support.focusinBubbles = "onfocusin" in window; +} )(); var rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; function returnTrue() { return true; @@ -4071,12 +4435,75 @@ function returnFalse() { return false; } +// Support: IE9 +// See #13393 for more info function safeActiveElement() { try { return document.activeElement; } catch ( err ) { } } +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + /* * Helper functions for managing events -- not part of the public interface. * Props to Dean Edwards' addEvent library for many of the ideas. @@ -4090,7 +4517,7 @@ jQuery.event = { var handleObjIn, eventHandle, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, - elemData = data_priv.get( elem ); + elemData = dataPriv.get( elem ); // Don't attach events to noData or text/comment nodes (but allow plain objects) if ( !elemData ) { @@ -4110,14 +4537,15 @@ jQuery.event = { } // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { + if ( !( events = elemData.events ) ) { events = elemData.events = {}; } - if ( !(eventHandle = elemData.handle) ) { + if ( !( eventHandle = elemData.handle ) ) { eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded - return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? jQuery.event.dispatch.apply( elem, arguments ) : undefined; }; } @@ -4126,9 +4554,9 @@ jQuery.event = { types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); // There *must* be a type, no attaching namespace-only handlers if ( !type ) { @@ -4145,7 +4573,7 @@ jQuery.event = { special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers - handleObj = jQuery.extend({ + handleObj = jQuery.extend( { type: type, origType: origType, data: data, @@ -4153,18 +4581,20 @@ jQuery.event = { guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") + namespace: namespaces.join( "." ) }, handleObjIn ); // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { + if ( !( handlers = events[ type ] ) ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); + elem.addEventListener( type, eventHandle ); } } } @@ -4196,9 +4626,9 @@ jQuery.event = { var j, origCount, tmp, events, t, handleObj, special, handlers, type, namespaces, origType, - elemData = data_priv.hasData( elem ) && data_priv.get( elem ); + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); - if ( !elemData || !(events = elemData.events) ) { + if ( !elemData || !( events = elemData.events ) ) { return; } @@ -4206,9 +4636,9 @@ jQuery.event = { types = ( types || "" ).match( rnotwhite ) || [ "" ]; t = types.length; while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); // Unbind all events (on this namespace, if provided) for the element if ( !type ) { @@ -4221,7 +4651,8 @@ jQuery.event = { special = jQuery.event.special[ type ] || {}; type = ( selector ? special.delegateType : special.bindType ) || type; handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); // Remove matching events origCount = j = handlers.length; @@ -4231,7 +4662,8 @@ jQuery.event = { if ( ( mappedTypes || origType === handleObj.origType ) && ( !handler || handler.guid === handleObj.guid ) && ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { handlers.splice( j, 1 ); if ( handleObj.selector ) { @@ -4246,7 +4678,9 @@ jQuery.event = { // Remove generic event handler if we removed something and no more handlers exist // (avoids potential for endless recursion during removal of special event handlers) if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); } @@ -4254,203 +4688,71 @@ jQuery.event = { } } - // Remove the expando if it's no longer used + // Remove data and the expando if it's no longer used if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - data_priv.remove( elem, "events" ); + dataPriv.remove( elem, "handle events" ); } }, - trigger: function( event, data, elem, onlyHandlers ) { + dispatch: function( event ) { - var i, cur, tmp, bubbleType, ontype, handle, special, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); - cur = tmp = elem = elem || document; + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } + event.handleObj = handleObj; + event.data = handleObj.data; - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } } + } - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); } - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && jQuery.acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && - jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - elem[ type ](); - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, j, ret, matched, handleObj, - handlerQueue = [], - args = slice.call( arguments ), - handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or 2) have namespace(s) - // a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, + return event.result; + }, handlers: function( event, handlers ) { var i, matches, sel, handleObj, @@ -4458,15 +4760,20 @@ jQuery.event = { delegateCount = handlers.delegateCount, cur = event.target; + // Support (at least): Chrome, IE9 // Find delegate handlers // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { for ( ; cur !== this; cur = cur.parentNode || this ) { + // Don't check non-elements (#13208) // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.disabled !== true || event.type !== "click" ) { + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; @@ -4476,7 +4783,7 @@ jQuery.event = { if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : + jQuery( sel, this ).index( cur ) > -1 : jQuery.find( sel, this, null, [ cur ] ).length; } if ( matches[ sel ] ) { @@ -4484,7 +4791,7 @@ jQuery.event = { } } if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); + handlerQueue.push( { elem: cur, handlers: matches } ); } } } @@ -4492,19 +4799,20 @@ jQuery.event = { // Add the remaining (directly-bound) handlers if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); } return handlerQueue; }, // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), fixHooks: {}, keyHooks: { - props: "char charCode key keyCode".split(" "), + props: "char charCode key keyCode".split( " " ), filter: function( event, original ) { // Add which for key events @@ -4517,7 +4825,8 @@ jQuery.event = { }, mouseHooks: { - props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), filter: function( event, original ) { var eventDoc, doc, body, button = original.button; @@ -4528,8 +4837,12 @@ jQuery.event = { doc = eventDoc.documentElement; body = eventDoc.body; - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); } // Add which for click: 1 === left; 2 === middle; 3 === right @@ -4586,10 +4899,12 @@ jQuery.event = { special: { load: { + // Prevent triggered image.load events from bubbling to window.load noBubble: true }, focus: { + // Fire native event if possible so blur/focus sequence is correct trigger: function() { if ( this !== safeActiveElement() && this.focus ) { @@ -4609,6 +4924,7 @@ jQuery.event = { delegateType: "focusout" }, click: { + // For checkbox, fire native event so checked state will be right trigger: function() { if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { @@ -4633,41 +4949,21 @@ jQuery.event = { } } } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } } }; jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); + elem.removeEventListener( type, handle ); } }; jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { + if ( !( this instanceof jQuery.Event ) ) { return new jQuery.Event( src, props ); } @@ -4680,6 +4976,7 @@ jQuery.Event = function( src, props ) { // by a handler lower down the tree; reflect the correct value. this.isDefaultPrevented = src.defaultPrevented || src.defaultPrevented === undefined && + // Support: Android<4.0 src.returnValue === false ? returnTrue : @@ -4705,6 +5002,7 @@ jQuery.Event = function( src, props ) { // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html jQuery.Event.prototype = { + constructor: jQuery.Event, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse, @@ -4714,7 +5012,7 @@ jQuery.Event.prototype = { this.isDefaultPrevented = returnTrue; - if ( e && e.preventDefault ) { + if ( e ) { e.preventDefault(); } }, @@ -4723,7 +5021,7 @@ jQuery.Event.prototype = { this.isPropagationStopped = returnTrue; - if ( e && e.stopPropagation ) { + if ( e ) { e.stopPropagation(); } }, @@ -4732,7 +5030,7 @@ jQuery.Event.prototype = { this.isImmediatePropagationStopped = returnTrue; - if ( e && e.stopImmediatePropagation ) { + if ( e ) { e.stopImmediatePropagation(); } @@ -4741,8 +5039,14 @@ jQuery.Event.prototype = { }; // Create mouseenter/leave events using mouseover/out and event-time checks -// Support: Chrome 15+ -jQuery.each({ +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { mouseenter: "mouseover", mouseleave: "mouseout", pointerenter: "pointerover", @@ -4758,9 +5062,9 @@ jQuery.each({ related = event.relatedTarget, handleObj = event.handleObj; - // For mousenter/leave call the handler if related is outside the target. + // For mouseenter/leave call the handler if related is outside the target. // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { event.type = handleObj.origType; ret = handleObj.handler.apply( this, arguments ); event.type = fix; @@ -4768,115 +5072,32 @@ jQuery.each({ return ret; } }; -}); - -// Support: Firefox, Chrome, Safari -// Create "bubbling" focus and blur events -if ( !support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = data_priv.access( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = data_priv.access( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - data_priv.remove( doc, fix ); - - } else { - data_priv.access( doc, fix, attaches ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } +} ); - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); }, one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); + return on( this, types, selector, data, fn, 1 ); }, off: function( types, selector, fn ) { var handleObj, type; if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event handleObj = types.handleObj; jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, handleObj.selector, handleObj.handler ); return this; } if ( typeof types === "object" ) { + // ( types-object [, selector] ) for ( type in types ) { this.off( type, selector, types[ type ] ); @@ -4884,6 +5105,7 @@ jQuery.fn.extend({ return this; } if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) fn = selector; selector = undefined; @@ -4891,70 +5113,39 @@ jQuery.fn.extend({ if ( fn === false ) { fn = returnFalse; } - return this.each(function() { + return this.each( function() { jQuery.event.remove( this, types, fn, selector ); - }); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } + } ); } -}); +} ); var - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rhtml = /<|?\w+;/, - rnoInnerhtml = /<(?:script|style|link)/i, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /'), FALSE, 'Rendering an external JavaScript file.'); - } - - /** - * Tests adding JavaScript settings. - */ - function testAddJsSettings() { - // Add a file in order to test default settings. - $build['#attached']['library'][] = 'core/drupalSettings'; - $assets = AttachedAssets::createFromRenderArray($build); - - $this->assertEqual([], $assets->getSettings(), 'JavaScript settings on $assets are empty.'); - $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $this->assertTrue(array_key_exists('currentPath', $javascript['drupalSettings']['data']['path']), 'The current path JavaScript setting is set correctly.'); - $this->assertTrue(array_key_exists('currentPath', $assets->getSettings()['path']), 'JavaScript settings on $assets are resolved after retrieving JavaScript assets, and are equal to the returned JavaScript settings.'); - - $assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]); - $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $this->assertEqual(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.'); - $this->assertEqual('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.'); - } - - /** - * Tests adding external CSS and JavaScript files. - */ - function testAddExternalFiles() { - $build['#attached']['library'][] = 'common_test/external'; - $assets = AttachedAssets::createFromRenderArray($build); - - $css = $this->assetResolver->getCssAssets($assets, FALSE); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $this->assertTrue(array_key_exists('http://example.com/stylesheet.css', $css), 'External CSS files are correctly added.'); - $this->assertTrue(array_key_exists('http://example.com/script.js', $js), 'External JavaScript files are correctly added.'); - - $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_css = $this->renderer->renderPlain($css_render_array); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'Rendering an external CSS file.'); - $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'Rendering an external JavaScript file.'); - } - - /** - * Tests adding JavaScript files with additional attributes. - */ - function testAttributes() { - $build['#attached']['library'][] = 'common_test/js-attributes'; - $assets = AttachedAssets::createFromRenderArray($build); - - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $expected_1 = ''; - $expected_2 = ''; - $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.'); - $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.'); - } - - /** - * Tests that attributes are maintained when JS aggregation is enabled. - */ - function testAggregatedAttributes() { - $build['#attached']['library'][] = 'common_test/js-attributes'; - $assets = AttachedAssets::createFromRenderArray($build); - - $js = $this->assetResolver->getJsAssets($assets, TRUE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $expected_1 = ''; - $expected_2 = ''; - $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.'); - $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.'); - } - - /** - * Integration test for CSS/JS aggregation. - */ - function testAggregation() { - $build['#attached']['library'][] = 'core/drupal.timezone'; - $build['#attached']['library'][] = 'core/drupal.vertical-tabs'; - $assets = AttachedAssets::createFromRenderArray($build); - - $this->assertEqual(1, count($this->assetResolver->getCssAssets($assets, TRUE)), 'There is a sole aggregated CSS asset.'); - - list($header_js, $footer_js) = $this->assetResolver->getJsAssets($assets, TRUE); - $this->assertEqual([], \Drupal::service('asset.js.collection_renderer')->render($header_js), 'There are 0 JavaScript assets in the header.'); - $rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js); - $this->assertEqual(2, count($rendered_footer_js), 'There are 2 JavaScript assets in the footer.'); - $this->assertEqual('drupal-settings-json', $rendered_footer_js[0]['#attributes']['data-drupal-selector'], 'The first of the two JavaScript assets in the footer has drupal settings.'); - $this->assertEqual(0, strpos($rendered_footer_js[1]['#attributes']['src'], base_path()), 'The second of the two JavaScript assets in the footer has the sole aggregated JavaScript asset.'); - } - - /** - * Tests JavaScript settings. - */ - function testSettings() { - $build = array(); - $build['#attached']['library'][] = 'core/drupalSettings'; - // Nonsensical value to verify if it's possible to override path settings. - $build['#attached']['drupalSettings']['path']['pathPrefix'] = 'yarhar'; - $assets = AttachedAssets::createFromRenderArray($build); - - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - - // Parse the generated drupalSettings '), FALSE, 'The JS asset in common_test/js-header appears in the header.'); - $this->assertNotIdentical(strpos($rendered_js, '' . "\n"; - $expected_2 = "\n" . '' . "\n"; - - $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered JavaScript within downlevel-hidden conditional comments.'); - $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered JavaScript within downlevel-revealed conditional comments.'); - } - - /** - * Tests JavaScript versioning. - */ - function testVersionQueryString() { - $build['#attached']['library'][] = 'core/backbone'; - $build['#attached']['library'][] = 'core/domready'; - $assets = AttachedAssets::createFromRenderArray($build); - - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - - $rendered_js = $this->renderer->renderPlain($js_render_array); - $this->assertTrue(strpos($rendered_js, 'core/assets/vendor/backbone/backbone-min.js?v=1.2.3') > 0 && strpos($rendered_js, 'core/assets/vendor/domready/ready.min.js?v=1.0.8') > 0, 'JavaScript version identifiers correctly appended to URLs'); - } - - /** - * Tests JavaScript and CSS asset ordering. - */ - function testRenderOrder() { - $build['#attached']['library'][] = 'common_test/order'; - $assets = AttachedAssets::createFromRenderArray($build); - - // Construct the expected result from the regex. - $expected_order_js = [ - "-8_1", - "-8_2", - "-8_3", - "-8_4", - "-5_1", // The external script. - "-3_1", - "-3_2", - "0_1", - "0_2", - "0_3", - ]; - - // Retrieve the rendered JavaScript and test against the regex. - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $matches = array(); - if (preg_match_all('/weight_([-0-9]+_[0-9]+)/', $rendered_js, $matches)) { - $result = $matches[1]; - } - else { - $result = array(); - } - $this->assertIdentical($result, $expected_order_js, 'JavaScript is added in the expected weight order.'); - - // Construct the expected result from the regex. - $expected_order_css = [ - // Base. - 'base_weight_-101_1', - 'base_weight_-8_1', - 'layout_weight_-101_1', - 'base_weight_0_1', - 'base_weight_0_2', - // Layout. - 'layout_weight_-8_1', - 'component_weight_-101_1', - 'layout_weight_0_1', - 'layout_weight_0_2', - // Component. - 'component_weight_-8_1', - 'state_weight_-101_1', - 'component_weight_0_1', - 'component_weight_0_2', - // State. - 'state_weight_-8_1', - 'theme_weight_-101_1', - 'state_weight_0_1', - 'state_weight_0_2', - // Theme. - 'theme_weight_-8_1', - 'theme_weight_0_1', - 'theme_weight_0_2', - ]; - - // Retrieve the rendered CSS and test against the regex. - $css = $this->assetResolver->getCssAssets($assets, FALSE); - $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); - $rendered_css = $this->renderer->renderPlain($css_render_array); - $matches = array(); - if (preg_match_all('/([a-z]+)_weight_([-0-9]+_[0-9]+)/', $rendered_css, $matches)) { - $result = $matches[0]; - } - else { - $result = array(); - } - $this->assertIdentical($result, $expected_order_css, 'CSS is added in the expected weight order.'); - } - - /** - * Tests rendering the JavaScript with a file's weight above jQuery's. - */ - function testRenderDifferentWeight() { - // If a library contains assets A and B, and A is listed first, then B can - // still make itself appear first by defining a lower weight. - $build['#attached']['library'][] = 'core/jquery'; - $build['#attached']['library'][] = 'common_test/weight'; - $assets = AttachedAssets::createFromRenderArray($build); - - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $this->assertTrue(strpos($rendered_js, 'lighter.css') < strpos($rendered_js, 'first.js'), 'Lighter CSS assets are rendered first.'); - $this->assertTrue(strpos($rendered_js, 'lighter.js') < strpos($rendered_js, 'first.js'), 'Lighter JavaScript assets are rendered first.'); - $this->assertTrue(strpos($rendered_js, 'before-jquery.js') < strpos($rendered_js, 'core/assets/vendor/jquery/jquery.min.js'), 'Rendering a JavaScript file above jQuery.'); - } - - /** - * Tests altering a JavaScript's weight via hook_js_alter(). - * - * @see simpletest_js_alter() - */ - function testAlter() { - // Add both tableselect.js and simpletest.js. - $build['#attached']['library'][] = 'core/drupal.tableselect'; - $build['#attached']['library'][] = 'simpletest/drupal.simpletest'; - $assets = AttachedAssets::createFromRenderArray($build); - - // Render the JavaScript, testing if simpletest.js was altered to be before - // tableselect.js. See simpletest_js_alter() to see where this alteration - // takes place. - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $this->assertTrue(strpos($rendered_js, 'simpletest.js') < strpos($rendered_js, 'core/misc/tableselect.js'), 'Altering JavaScript weight through the alter hook.'); - } - - /** - * Adds a JavaScript library to the page and alters it. - * - * @see common_test_library_info_alter() - */ - function testLibraryAlter() { - // Verify that common_test altered the title of Farbtastic. - /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ - $library_discovery = \Drupal::service('library.discovery'); - $library = $library_discovery->getLibraryByName('core', 'jquery.farbtastic'); - $this->assertEqual($library['version'], '0.0', 'Registered libraries were altered.'); - - // common_test_library_info_alter() also added a dependency on jQuery Form. - $build['#attached']['library'][] = 'core/jquery.farbtastic'; - $assets = AttachedAssets::createFromRenderArray($build); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $this->assertTrue(strpos($rendered_js, 'core/assets/vendor/jquery-form/jquery.form.min.js'), 'Altered library dependencies are added to the page.'); - } - - /** - * Dynamically defines an asset library and alters it. - */ - function testDynamicLibrary() { - /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ - $library_discovery = \Drupal::service('library.discovery'); - // Retrieve a dynamic library definition. - // @see common_test_library_info_build() - \Drupal::state()->set('common_test.library_info_build_test', TRUE); - $library_discovery->clearCachedDefinitions(); - $dynamic_library = $library_discovery->getLibraryByName('common_test', 'dynamic_library'); - $this->assertTrue(is_array($dynamic_library)); - if ($this->assertTrue(isset($dynamic_library['version']))) { - $this->assertIdentical('1.0', $dynamic_library['version']); - } - // Make sure the dynamic library definition could be altered. - // @see common_test_library_info_alter() - if ($this->assertTrue(isset($dynamic_library['dependencies']))) { - $this->assertIdentical(['core/jquery'], $dynamic_library['dependencies']); - } - } - - /** - * Tests that multiple modules can implement libraries with the same name. - * - * @see common_test.library.yml - */ - function testLibraryNameConflicts() { - /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ - $library_discovery = \Drupal::service('library.discovery'); - $farbtastic = $library_discovery->getLibraryByName('common_test', 'jquery.farbtastic'); - $this->assertEqual($farbtastic['version'], '0.1', 'Alternative libraries can be added to the page.'); - } - - /** - * Tests JavaScript files that have querystrings attached get added right. - */ - function testAddJsFileWithQueryString() { - $build['#attached']['library'][] = 'common_test/querystring'; - $assets = AttachedAssets::createFromRenderArray($build); - - $css = $this->assetResolver->getCssAssets($assets, FALSE); - $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; - $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css), 'CSS file with query string is correctly added.'); - $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js), 'JavaScript file with query string is correctly added.'); - - $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); - $rendered_css = $this->renderer->renderPlain($css_render_array); - $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); - $rendered_js = $this->renderer->renderPlain($js_render_array); - $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; - $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'CSS file with query string gets version query string correctly appended..'); - $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'JavaScript file with query string gets version query string correctly appended.'); - } - -} diff --git a/core/modules/system/src/Tests/Common/PageRenderTest.php b/core/modules/system/src/Tests/Common/PageRenderTest.php index 8906982..f07ece6 100644 --- a/core/modules/system/src/Tests/Common/PageRenderTest.php +++ b/core/modules/system/src/Tests/Common/PageRenderTest.php @@ -2,7 +2,7 @@ namespace Drupal\system\Tests\Common; -use Drupal\simpletest\KernelTestBase; +use Drupal\KernelTests\KernelTestBase; /** * Test page rendering hooks. diff --git a/core/modules/system/src/Tests/Common/RenderElementTypesTest.php b/core/modules/system/src/Tests/Common/RenderElementTypesTest.php deleted file mode 100644 index e60bb52..0000000 --- a/core/modules/system/src/Tests/Common/RenderElementTypesTest.php +++ /dev/null @@ -1,243 +0,0 @@ -installConfig(array('system')); - \Drupal::service('router.builder')->rebuild(); - } - - /** - * Asserts that an array of elements is rendered properly. - * - * @param array $elements - * The render element array to test. - * @param string $expected_html - * The expected markup. - * @param string $message - * Assertion message. - */ - protected function assertElements(array $elements, $expected_html, $message) { - $actual_html = (string) \Drupal::service('renderer')->renderRoot($elements); - - $out = ''; - $out .= '' . Html::escape($expected_html) . ''; - $out .= '' . Html::escape($actual_html) . ''; - $out .= ''; - $this->verbose($out); - - $this->assertIdentical($actual_html, $expected_html, Html::escape($message)); - } - - /** - * Tests system #type 'container'. - */ - function testContainer() { - // Basic container with no attributes. - $this->assertElements(array( - '#type' => 'container', - '#markup' => 'foo', - ), "foo\n", "#type 'container' with no HTML attributes"); - - // Container with a class. - $this->assertElements(array( - '#type' => 'container', - '#markup' => 'foo', - '#attributes' => array( - 'class' => array('bar'), - ), - ), 'foo' . "\n", "#type 'container' with a class HTML attribute"); - - // Container with children. - $this->assertElements(array( - '#type' => 'container', - 'child' => array( - '#markup' => 'foo', - ), - ), "foo\n", "#type 'container' with child elements"); - } - - /** - * Tests system #type 'html_tag'. - */ - function testHtmlTag() { - // Test void element. - $this->assertElements(array( - '#type' => 'html_tag', - '#tag' => 'meta', - '#value' => 'ignored', - '#attributes' => array( - 'name' => 'description', - 'content' => 'Drupal test', - ), - ), '' . "\n", "#type 'html_tag', void element renders properly"); - - // Test non-void element. - $this->assertElements(array( - '#type' => 'html_tag', - '#tag' => 'section', - '#value' => 'value', - '#attributes' => array( - 'class' => array('unicorns'), - ), - ), 'value' . "\n", "#type 'html_tag', non-void element renders properly"); - - // Test empty void element tag. - $this->assertElements(array( - '#type' => 'html_tag', - '#tag' => 'link', - ), "\n", "#type 'html_tag' empty void element renders properly"); - - // Test empty non-void element tag. - $this->assertElements(array( - '#type' => 'html_tag', - '#tag' => 'section', - ), "\n", "#type 'html_tag' empty non-void element renders properly"); - } - - /** - * Tests system #type 'more_link'. - */ - function testMoreLink() { - $elements = array( - array( - 'name' => "#type 'more_link' anchor tag generation without extra classes", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromUri('https://www.drupal.org'), - ), - 'expected' => '//div[@class="more-link"]/a[@href="https://www.drupal.org" and text()="More"]', - ), - array( - 'name' => "#type 'more_link' anchor tag generation with different link text", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromUri('https://www.drupal.org'), - '#title' => 'More Titles', - ), - 'expected' => '//div[@class="more-link"]/a[@href="https://www.drupal.org" and text()="More Titles"]', - ), - array( - 'name' => "#type 'more_link' anchor tag generation with attributes on wrapper", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromUri('https://www.drupal.org'), - '#theme_wrappers' => array( - 'container' => array( - '#attributes' => array( - 'title' => 'description', - 'class' => array('more-link', 'drupal', 'test'), - ), - ), - ), - ), - 'expected' => '//div[@title="description" and contains(@class, "more-link") and contains(@class, "drupal") and contains(@class, "test")]/a[@href="https://www.drupal.org" and text()="More"]', - ), - array( - 'name' => "#type 'more_link' anchor tag with a relative path", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromRoute('router_test.1'), - ), - 'expected' => '//div[@class="more-link"]/a[@href="' . Url::fromRoute('router_test.1')->toString() . '" and text()="More"]', - ), - array( - 'name' => "#type 'more_link' anchor tag with a route", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromRoute('router_test.1'), - ), - 'expected' => '//div[@class="more-link"]/a[@href="' . \Drupal::urlGenerator()->generate('router_test.1') . '" and text()="More"]', - ), - array( - 'name' => "#type 'more_link' anchor tag with an absolute path", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromRoute('system.admin_content'), - '#options' => array('absolute' => TRUE), - ), - 'expected' => '//div[@class="more-link"]/a[@href="' . Url::fromRoute('system.admin_content')->setAbsolute()->toString() . '" and text()="More"]', - ), - array( - 'name' => "#type 'more_link' anchor tag to the front page", - 'value' => array( - '#type' => 'more_link', - '#url' => Url::fromRoute(''), - ), - 'expected' => '//div[@class="more-link"]/a[@href="' . Url::fromRoute('')->toString() . '" and text()="More"]', - ), - ); - - foreach ($elements as $element) { - $xml = new \SimpleXMLElement(\Drupal::service('renderer')->renderRoot($element['value'])); - $result = $xml->xpath($element['expected']); - $this->assertTrue($result, '"' . $element['name'] . '" input rendered correctly by drupal_render().'); - } - } - - /** - * Tests system #type 'system_compact_link'. - */ - function testSystemCompactLink() { - $elements = array( - array( - 'name' => "#type 'system_compact_link' when admin compact mode is off", - 'value' => array( - '#type' => 'system_compact_link', - ), - 'expected' => '//div[@class="compact-link"]/a[contains(@href, "admin/compact/on?") and text()="Hide descriptions"]', - ), - array( - 'name' => "#type 'system_compact_link' when adding extra attributes", - 'value' => array( - '#type' => 'system_compact_link', - '#attributes' => array( - 'class' => array('kittens-rule'), - ), - ), - 'expected' => '//div[@class="compact-link"]/a[contains(@href, "admin/compact/on?") and @class="kittens-rule" and text()="Hide descriptions"]', - ), - ); - - foreach ($elements as $element) { - $xml = new \SimpleXMLElement(\Drupal::service('renderer')->renderRoot($element['value'])); - $result = $xml->xpath($element['expected']); - $this->assertTrue($result, '"' . $element['name'] . '" is rendered correctly by drupal_render().'); - } - - // Set admin compact mode on for additional tests. - \Drupal::request()->cookies->set('Drupal_visitor_admin_compact_mode', TRUE); - - $element = array( - 'name' => "#type 'system_compact_link' when admin compact mode is on", - 'value' => array( - '#type' => 'system_compact_link', - ), - 'expected' => '//div[@class="compact-link"]/a[contains(@href, "admin/compact?") and text()="Show descriptions"]', - ); - - $xml = new \SimpleXMLElement(\Drupal::service('renderer')->renderRoot($element['value'])); - $result = $xml->xpath($element['expected']); - $this->assertTrue($result, '"' . $element['name'] . '" is rendered correctly by drupal_render().'); - } - -} diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php deleted file mode 100644 index 1eee23e..0000000 --- a/core/modules/system/src/Tests/Common/RenderTest.php +++ /dev/null @@ -1,63 +0,0 @@ -set('theme_preprocess_attached_test', TRUE); - - $test_element = [ - '#theme' => 'common_test_render_element', - 'foo' => [ - '#markup' => 'Kittens!', - ], - ]; - \Drupal::service('renderer')->renderRoot($test_element); - - $expected_attached = [ - 'library' => [ - 'test/generic_preprocess', - 'test/specific_preprocess', - ] - ]; - $this->assertEqual($expected_attached, $test_element['#attached'], 'All expected assets from theme preprocess hooks attached.'); - - \Drupal::state()->set('theme_preprocess_attached_test', FALSE); - } - - /** - * Tests that we get an exception when we try to attach an illegal type. - */ - public function testProcessAttached() { - // Specify invalid attachments in a render array. - $build['#attached']['library'][] = 'core/drupal.states'; - $build['#attached']['drupal_process_states'][] = []; - $renderer = $this->container->get('bare_html_page_renderer'); - try { - $renderer->renderBarePage($build, '', 'maintenance_page'); - $this->fail("Invalid #attachment 'drupal_process_states' allowed"); - } - catch (\LogicException $e) { - $this->pass("Invalid #attachment 'drupal_process_states' not allowed"); - } - } - -} diff --git a/core/modules/system/src/Tests/Common/SizeUnitTest.php b/core/modules/system/src/Tests/Common/SizeUnitTest.php deleted file mode 100644 index 39c06bb..0000000 --- a/core/modules/system/src/Tests/Common/SizeUnitTest.php +++ /dev/null @@ -1,69 +0,0 @@ -exactTestCases = array( - '1 byte' => 1, - '1 KB' => $kb, - '1 MB' => $kb * $kb, - '1 GB' => $kb * $kb * $kb, - '1 TB' => $kb * $kb * $kb * $kb, - '1 PB' => $kb * $kb * $kb * $kb * $kb, - '1 EB' => $kb * $kb * $kb * $kb * $kb * $kb, - '1 ZB' => $kb * $kb * $kb * $kb * $kb * $kb * $kb, - '1 YB' => $kb * $kb * $kb * $kb * $kb * $kb * $kb * $kb, - ); - $this->roundedTestCases = array( - '2 bytes' => 2, - '1 MB' => ($kb * $kb) - 1, // rounded to 1 MB (not 1000 or 1024 kilobyte!) - round(3623651 / ($this->exactTestCases['1 MB']), 2) . ' MB' => 3623651, // megabytes - round(67234178751368124 / ($this->exactTestCases['1 PB']), 2) . ' PB' => 67234178751368124, // petabytes - round(235346823821125814962843827 / ($this->exactTestCases['1 YB']), 2) . ' YB' => 235346823821125814962843827, // yottabytes - ); - } - - /** - * Checks that format_size() returns the expected string. - */ - function testCommonFormatSize() { - foreach (array($this->exactTestCases, $this->roundedTestCases) as $test_cases) { - foreach ($test_cases as $expected => $input) { - $this->assertEqual( - ($result = format_size($input, NULL)), - $expected, - $expected . ' == ' . $result . ' (' . $input . ' bytes)' - ); - } - } - } - - /** - * Cross-tests Bytes::toInt() and format_size(). - */ - function testCommonParseSizeFormatSize() { - foreach ($this->exactTestCases as $size) { - $this->assertEqual( - $size, - ($parsed_size = Bytes::toInt($string = format_size($size, NULL))), - $size . ' == ' . $parsed_size . ' (' . $string . ')' - ); - } - } - -} diff --git a/core/modules/system/src/Tests/Common/SystemListingTest.php b/core/modules/system/src/Tests/Common/SystemListingTest.php index e2d5c6f..cf29be4 100644 --- a/core/modules/system/src/Tests/Common/SystemListingTest.php +++ b/core/modules/system/src/Tests/Common/SystemListingTest.php @@ -3,7 +3,7 @@ namespace Drupal\system\Tests\Common; use Drupal\Core\Extension\ExtensionDiscovery; -use Drupal\simpletest\KernelTestBase; +use Drupal\KernelTests\KernelTestBase; /** * Tests scanning system directories in drupal_system_listing(). diff --git a/core/modules/system/src/Tests/Common/TableSortExtenderUnitTest.php b/core/modules/system/src/Tests/Common/TableSortExtenderUnitTest.php deleted file mode 100644 index 9b456a9..0000000 --- a/core/modules/system/src/Tests/Common/TableSortExtenderUnitTest.php +++ /dev/null @@ -1,141 +0,0 @@ -query to prevent parameters from Simpletest and Batch API - // ending up in $ts['query']. - $expected_ts = array( - 'name' => 'foo', - 'sql' => '', - 'sort' => 'asc', - 'query' => array(), - ); - $request = Request::createFromGlobals(); - $request->query->replace(array()); - \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); - $this->verbose(strtr('$ts: !ts', array('!ts' => Html::escape(var_export($ts, TRUE))))); - $this->assertEqual($ts, $expected_ts, 'Simple table headers sorted correctly.'); - - // Test with simple table headers plus $_GET parameters that should _not_ - // override the default. - $request = Request::createFromGlobals(); - $request->query->replace(array( - // This should not override the table order because only complex - // headers are overridable. - 'order' => 'bar', - )); - \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); - $this->verbose(strtr('$ts: !ts', array('!ts' => Html::escape(var_export($ts, TRUE))))); - $this->assertEqual($ts, $expected_ts, 'Simple table headers plus non-overriding $_GET parameters sorted correctly.'); - - // Test with simple table headers plus $_GET parameters that _should_ - // override the default. - $request = Request::createFromGlobals(); - $request->query->replace(array( - 'sort' => 'DESC', - // Add an unrelated parameter to ensure that tablesort will include - // it in the links that it creates. - 'alpha' => 'beta', - )); - \Drupal::getContainer()->get('request_stack')->push($request); - $expected_ts['sort'] = 'desc'; - $expected_ts['query'] = array('alpha' => 'beta'); - $ts = tablesort_init($headers); - $this->verbose(strtr('$ts: !ts', array('!ts' => Html::escape(var_export($ts, TRUE))))); - $this->assertEqual($ts, $expected_ts, 'Simple table headers plus $_GET parameters sorted correctly.'); - - // Test complex table headers. - - $headers = array( - 'foo', - array( - 'data' => '1', - 'field' => 'one', - 'sort' => 'asc', - 'colspan' => 1, - ), - array( - 'data' => '2', - 'field' => 'two', - 'sort' => 'desc', - ), - ); - // Reset $_GET from previous assertion. - $request = Request::createFromGlobals(); - $request->query->replace(array( - 'order' => '2', - )); - \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); - $expected_ts = array( - 'name' => '2', - 'sql' => 'two', - 'sort' => 'desc', - 'query' => array(), - ); - $this->verbose(strtr('$ts: !ts', array('!ts' => Html::escape(var_export($ts, TRUE))))); - $this->assertEqual($ts, $expected_ts, 'Complex table headers sorted correctly.'); - - // Test complex table headers plus $_GET parameters that should _not_ - // override the default. - $request = Request::createFromGlobals(); - $request->query->replace(array( - // This should not override the table order because this header does not - // exist. - 'order' => 'bar', - )); - \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); - $expected_ts = array( - 'name' => '1', - 'sql' => 'one', - 'sort' => 'asc', - 'query' => array(), - ); - $this->verbose(strtr('$ts: !ts', array('!ts' => Html::escape(var_export($ts, TRUE))))); - $this->assertEqual($ts, $expected_ts, 'Complex table headers plus non-overriding $_GET parameters sorted correctly.'); - - // Test complex table headers plus $_GET parameters that _should_ - // override the default. - $request = Request::createFromGlobals(); - $request->query->replace(array( - 'order' => '1', - 'sort' => 'ASC', - // Add an unrelated parameter to ensure that tablesort will include - // it in the links that it creates. - 'alpha' => 'beta', - )); - \Drupal::getContainer()->get('request_stack')->push($request); - $expected_ts = array( - 'name' => '1', - 'sql' => 'one', - 'sort' => 'asc', - 'query' => array('alpha' => 'beta'), - ); - $ts = tablesort_init($headers); - $this->verbose(strtr('$ts: !ts', array('!ts' => Html::escape(var_export($ts, TRUE))))); - $this->assertEqual($ts, $expected_ts, 'Complex table headers plus $_GET parameters sorted correctly.'); - } - -} diff --git a/core/modules/system/src/Tests/Common/XssUnitTest.php b/core/modules/system/src/Tests/Common/XssUnitTest.php deleted file mode 100644 index 1797737..0000000 --- a/core/modules/system/src/Tests/Common/XssUnitTest.php +++ /dev/null @@ -1,57 +0,0 @@ -installConfig(array('system')); - } - - /** - * Tests t() functionality. - */ - function testT() { - $text = t('Simple text'); - $this->assertEqual($text, 'Simple text', 't leaves simple text alone.'); - $text = t('Escaped text: @value', array('@value' => '')) !== FALSE); - $this->assertTrue(strpos($response->getContent(), '') === FALSE); - } - - /** - * Tests exception message escaping. - */ - public function testExceptionEscaping() { - // Enable verbose error logging. - $this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save(); - - // Using SafeMarkup::format(). - $request = Request::create('/router_test/test24'); - $request->setFormat('html', ['text/html']); - - /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ - $kernel = \Drupal::getContainer()->get('http_kernel'); - $response = $kernel->handle($request)->prepare($request); - $this->assertEqual($response->getStatusCode(), Response::HTTP_INTERNAL_SERVER_ERROR); - $this->assertEqual($response->headers->get('Content-type'), 'text/html; charset=UTF-8'); - - // Test message is properly escaped, and that the unescaped string is not - // output at all. - $this->setRawContent($response->getContent()); - $this->assertRaw(Html::escape('Escaped content: ')); - $this->assertNoRaw(' '); - } - -} diff --git a/core/modules/system/src/Tests/Routing/MatcherDumperTest.php b/core/modules/system/src/Tests/Routing/MatcherDumperTest.php deleted file mode 100644 index 102aa1a..0000000 --- a/core/modules/system/src/Tests/Routing/MatcherDumperTest.php +++ /dev/null @@ -1,168 +0,0 @@ -fixtures = new RoutingFixtures(); - $this->state = new State(new KeyValueMemoryFactory()); - } - - /** - * Confirms that the dumper can be instantiated successfully. - */ - function testCreate() { - $connection = Database::getConnection(); - $dumper = new MatcherDumper($connection, $this->state); - - $class_name = 'Drupal\Core\Routing\MatcherDumper'; - $this->assertTrue($dumper instanceof $class_name, 'Dumper created successfully'); - } - - /** - * Confirms that we can add routes to the dumper. - */ - function testAddRoutes() { - $connection = Database::getConnection(); - $dumper = new MatcherDumper($connection, $this->state); - - $route = new Route('test'); - $collection = new RouteCollection(); - $collection->add('test_route', $route); - - $dumper->addRoutes($collection); - - $dumper_routes = $dumper->getRoutes()->all(); - $collection_routes = $collection->all(); - - foreach ($dumper_routes as $name => $route) { - $this->assertEqual($route->getPath(), $collection_routes[$name]->getPath(), 'Routes match'); - } - } - - /** - * Confirms that we can add routes to the dumper when it already has some. - */ - function testAddAdditionalRoutes() { - $connection = Database::getConnection(); - $dumper = new MatcherDumper($connection, $this->state); - - $route = new Route('test'); - $collection = new RouteCollection(); - $collection->add('test_route', $route); - $dumper->addRoutes($collection); - - $route = new Route('test2'); - $collection2 = new RouteCollection(); - $collection2->add('test_route2', $route); - $dumper->addRoutes($collection2); - - // Merge the two collections together so we can test them. - $collection->addCollection(clone $collection2); - - $dumper_routes = $dumper->getRoutes()->all(); - $collection_routes = $collection->all(); - - $success = TRUE; - foreach ($collection_routes as $name => $route) { - if (empty($dumper_routes[$name])) { - $success = FALSE; - $this->fail(t('Not all routes found in the dumper.')); - } - } - - if ($success) { - $this->pass('All routes found in the dumper.'); - } - } - - /** - * Confirm that we can dump a route collection to the database. - */ - public function testDump() { - $connection = Database::getConnection(); - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - - $route = new Route('/test/{my}/path'); - $route->setOption('compiler_class', 'Drupal\Core\Routing\RouteCompiler'); - $collection = new RouteCollection(); - $collection->add('test_route', $route); - - $dumper->addRoutes($collection); - - $this->fixtures->createTables($connection); - - $dumper->dump(array('provider' => 'test')); - - $record = $connection->query("SELECT * FROM {test_routes} WHERE name= :name", array(':name' => 'test_route'))->fetchObject(); - - $loaded_route = unserialize($record->route); - - $this->assertEqual($record->name, 'test_route', 'Dumped route has correct name.'); - $this->assertEqual($record->path, '/test/{my}/path', 'Dumped route has correct pattern.'); - $this->assertEqual($record->pattern_outline, '/test/%/path', 'Dumped route has correct pattern outline.'); - $this->assertEqual($record->fit, 5 /* 101 in binary */, 'Dumped route has correct fit.'); - $this->assertTrue($loaded_route instanceof Route, 'Route object retrieved successfully.'); - } - - /** - * Tests the determination of the masks generation. - */ - public function testMenuMasksGeneration() { - $connection = Database::getConnection(); - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - - $collection = new RouteCollection(); - $collection->add('test_route_1', new Route('/test-length-3/{my}/path')); - $collection->add('test_route_2', new Route('/test-length-3/hello/path')); - $collection->add('test_route_3', new Route('/test-length-5/{my}/path/marvin/magrathea')); - $collection->add('test_route_4', new Route('/test-length-7/{my}/path/marvin/magrathea/earth/ursa-minor')); - - $dumper->addRoutes($collection); - - $this->fixtures->createTables($connection); - - $dumper->dump(array('provider' => 'test')); - // Using binary for readability, we expect a 0 at any wildcard slug. They - // should be ordered from longest to shortest. - $expected = array( - bindec('1011111'), - bindec('10111'), - bindec('111'), - bindec('101'), - ); - $this->assertEqual($this->state->get('routing.menu_masks.test_routes'), $expected); - } - -} diff --git a/core/modules/system/src/Tests/Routing/RouteProviderTest.php b/core/modules/system/src/Tests/Routing/RouteProviderTest.php deleted file mode 100644 index 056a37a..0000000 --- a/core/modules/system/src/Tests/Routing/RouteProviderTest.php +++ /dev/null @@ -1,630 +0,0 @@ -fixtures = new RoutingFixtures(); - $this->state = new State(new KeyValueMemoryFactory()); - $this->currentPath = new CurrentPathStack(new RequestStack()); - $this->cache = new MemoryBackend('data'); - $this->pathProcessor = \Drupal::service('path_processor_manager'); - $this->cacheTagsInvalidator = \Drupal::service('cache_tags.invalidator'); - } - - /** - * {@inheritdoc} - */ - public function containerBuild(ContainerBuilder $container) { - parent::containerBuild($container); // TODO: Change the autogenerated stub - - // Readd the incoming path alias for these tests. - if ($container->hasDefinition('path_processor_alias')) { - $definition = $container->getDefinition('path_processor_alias'); - $definition->addTag('path_processor_inbound'); - } - } - - protected function tearDown() { - $this->fixtures->dropTables(Database::getConnection()); - - parent::tearDown(); - } - - /** - * Confirms that the correct candidate outlines are generated. - */ - public function testCandidateOutlines() { - - $connection = Database::getConnection(); - $provider = new TestRouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $parts = array('node', '5', 'edit'); - - $candidates = $provider->getCandidateOutlines($parts); - - $candidates = array_flip($candidates); - - $this->assertTrue(count($candidates) == 7, 'Correct number of candidates found'); - $this->assertTrue(array_key_exists('/node/5/edit', $candidates), 'First candidate found.'); - $this->assertTrue(array_key_exists('/node/5/%', $candidates), 'Second candidate found.'); - $this->assertTrue(array_key_exists('/node/%/edit', $candidates), 'Third candidate found.'); - $this->assertTrue(array_key_exists('/node/%/%', $candidates), 'Fourth candidate found.'); - $this->assertTrue(array_key_exists('/node/5', $candidates), 'Fifth candidate found.'); - $this->assertTrue(array_key_exists('/node/%', $candidates), 'Sixth candidate found.'); - $this->assertTrue(array_key_exists('/node', $candidates), 'Seventh candidate found.'); - } - - /** - * Don't fail when given an empty path. - */ - public function testEmptyPathCandidatesOutlines() { - $provider = new TestRouteProvider(Database::getConnection(), $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - $candidates = $provider->getCandidateOutlines([]); - $this->assertEqual(count($candidates), 0, 'Empty parts should return no candidates.'); - } - - /** - * Confirms that we can find routes with the exact incoming path. - */ - function testExactPathMatch() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->sampleRouteCollection()); - $dumper->dump(); - - $path = '/path/one'; - - $request = Request::create($path, 'GET'); - - $routes = $provider->getRouteCollectionForRequest($request); - - foreach ($routes as $route) { - $this->assertEqual($route->getPath(), $path, 'Found path has correct pattern'); - } - } - - /** - * Confirms that we can find routes whose pattern would match the request. - */ - function testOutlinePathMatch() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->complexRouteCollection()); - $dumper->dump(); - - $path = '/path/1/one'; - - $request = Request::create($path, 'GET'); - - $routes = $provider->getRouteCollectionForRequest($request); - - // All of the matching paths have the correct pattern. - foreach ($routes as $route) { - $this->assertEqual($route->compile()->getPatternOutline(), '/path/%/one', 'Found path has correct pattern'); - } - - $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); - $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.'); - $this->assertNotNull($routes->get('route_b'), 'The second matching route was not found.'); - } - - /** - * Confirms that a trailing slash on the request doesn't result in a 404. - */ - function testOutlinePathMatchTrailingSlash() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->complexRouteCollection()); - $dumper->dump(); - - $path = '/path/1/one/'; - - $request = Request::create($path, 'GET'); - - $routes = $provider->getRouteCollectionForRequest($request); - - // All of the matching paths have the correct pattern. - foreach ($routes as $route) { - $this->assertEqual($route->compile()->getPatternOutline(), '/path/%/one', 'Found path has correct pattern'); - } - - $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); - $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.'); - $this->assertNotNull($routes->get('route_b'), 'The second matching route was not found.'); - } - - /** - * Confirms that we can find routes whose pattern would match the request. - */ - function testOutlinePathMatchDefaults() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $collection = new RouteCollection(); - $collection->add('poink', new Route('/some/path/{value}', array( - 'value' => 'poink', - ))); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($collection); - $dumper->dump(); - - $path = '/some/path'; - - $request = Request::create($path, 'GET'); - - try { - $routes = $provider->getRouteCollectionForRequest($request); - - // All of the matching paths have the correct pattern. - foreach ($routes as $route) { - $this->assertEqual($route->compile()->getPatternOutline(), '/some/path', 'Found path has correct pattern'); - } - - $this->assertEqual(count($routes), 1, 'The correct number of routes was found.'); - $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); - } - catch (ResourceNotFoundException $e) { - $this->fail('No matching route found with default argument value.'); - } - } - - /** - * Confirms that we can find routes whose pattern would match the request. - */ - function testOutlinePathMatchDefaultsCollision() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $collection = new RouteCollection(); - $collection->add('poink', new Route('/some/path/{value}', array( - 'value' => 'poink', - ))); - $collection->add('narf', new Route('/some/path/here')); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($collection); - $dumper->dump(); - - $path = '/some/path'; - - $request = Request::create($path, 'GET'); - - try { - $routes = $provider->getRouteCollectionForRequest($request); - - // All of the matching paths have the correct pattern. - foreach ($routes as $route) { - $this->assertEqual($route->compile()->getPatternOutline(), '/some/path', 'Found path has correct pattern'); - } - - $this->assertEqual(count($routes), 1, 'The correct number of routes was found.'); - $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); - } - catch (ResourceNotFoundException $e) { - $this->fail('No matching route found with default argument value.'); - } - } - - /** - * Confirms that we can find routes whose pattern would match the request. - */ - function testOutlinePathMatchDefaultsCollision2() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $collection = new RouteCollection(); - $collection->add('poink', new Route('/some/path/{value}', array( - 'value' => 'poink', - ))); - $collection->add('narf', new Route('/some/path/here')); - $collection->add('eep', new Route('/something/completely/different')); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($collection); - $dumper->dump(); - - $path = '/some/path/here'; - - $request = Request::create($path, 'GET'); - - try { - $routes = $provider->getRouteCollectionForRequest($request); - $routes_array = $routes->all(); - - $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); - $this->assertEqual(array('narf', 'poink'), array_keys($routes_array), 'Ensure the fitness was taken into account.'); - $this->assertNotNull($routes->get('narf'), 'The first matching route was found.'); - $this->assertNotNull($routes->get('poink'), 'The second matching route was found.'); - $this->assertNull($routes->get('eep'), 'Non-matching route was not found.'); - } - catch (ResourceNotFoundException $e) { - $this->fail('No matching route found with default argument value.'); - } - } - - /** - * Confirms that we can find multiple routes that match the request equally. - */ - function testOutlinePathMatchDefaultsCollision3() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $collection = new RouteCollection(); - $collection->add('poink', new Route('/some/{value}/path')); - // Add a second route matching the same path pattern. - $collection->add('poink2', new Route('/some/{object}/path')); - $collection->add('narf', new Route('/some/here/path')); - $collection->add('eep', new Route('/something/completely/different')); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($collection); - $dumper->dump(); - - $path = '/some/over-there/path'; - - $request = Request::create($path, 'GET'); - - try { - $routes = $provider->getRouteCollectionForRequest($request); - $routes_array = $routes->all(); - - $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); - $this->assertEqual(array('poink', 'poink2'), array_keys($routes_array), 'Ensure the fitness and name were taken into account in the sort.'); - $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); - $this->assertNotNull($routes->get('poink2'), 'The second matching route was found.'); - $this->assertNull($routes->get('eep'), 'Non-matching route was not found.'); - } - catch (ResourceNotFoundException $e) { - $this->fail('No matching route found with default argument value.'); - } - } - - /** - * Tests a route with a 0 as value. - */ - public function testOutlinePathMatchZero() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $collection = new RouteCollection(); - $collection->add('poink', new Route('/some/path/{value}')); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($collection); - $dumper->dump(); - - $path = '/some/path/0'; - - $request = Request::create($path, 'GET'); - - try { - $routes = $provider->getRouteCollectionForRequest($request); - - // All of the matching paths have the correct pattern. - foreach ($routes as $route) { - $this->assertEqual($route->compile()->getPatternOutline(), '/some/path/%', 'Found path has correct pattern'); - } - - $this->assertEqual(count($routes), 1, 'The correct number of routes was found.'); - } - catch (ResourceNotFoundException $e) { - $this->fail('No matchout route found with 0 as argument value'); - } - } - - /** - * Confirms that an exception is thrown when no matching path is found. - */ - function testOutlinePathNoMatch() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->complexRouteCollection()); - $dumper->dump(); - - $path = '/no/such/path'; - - $request = Request::create($path, 'GET'); - - - $routes = $provider->getRoutesByPattern($path); - $this->assertFalse(count($routes), 'No path found with this pattern.'); - - $collection = $provider->getRouteCollectionForRequest($request); - $this->assertTrue(count($collection) == 0, 'Empty route collection found with this pattern.'); - } - - /** - * Tests that route caching works. - */ - public function testRouteCaching() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->sampleRouteCollection()); - $dumper->addRoutes($this->fixtures->complexRouteCollection()); - $dumper->dump(); - - // A simple path. - $path = '/path/add/one'; - $request = Request::create($path, 'GET'); - $provider->getRouteCollectionForRequest($request); - - $cache = $this->cache->get('route:/path/add/one:'); - $this->assertEqual('/path/add/one', $cache->data['path']); - $this->assertEqual([], $cache->data['query']); - $this->assertEqual(3, count($cache->data['routes'])); - - // A path with query parameters. - $path = '/path/add/one?foo=bar'; - $request = Request::create($path, 'GET'); - $provider->getRouteCollectionForRequest($request); - - $cache = $this->cache->get('route:/path/add/one:foo=bar'); - $this->assertEqual('/path/add/one', $cache->data['path']); - $this->assertEqual(['foo' => 'bar'], $cache->data['query']); - $this->assertEqual(3, count($cache->data['routes'])); - - // A path with placeholders. - $path = '/path/1/one'; - $request = Request::create($path, 'GET'); - $provider->getRouteCollectionForRequest($request); - - $cache = $this->cache->get('route:/path/1/one:'); - $this->assertEqual('/path/1/one', $cache->data['path']); - $this->assertEqual([], $cache->data['query']); - $this->assertEqual(2, count($cache->data['routes'])); - - // A path with a path alias. - /** @var \Drupal\Core\Path\AliasStorageInterface $path_storage */ - $path_storage = \Drupal::service('path.alias_storage'); - $path_storage->save('/path/add/one', '/path/add-one'); - /** @var \Drupal\Core\Path\AliasManagerInterface $alias_manager */ - $alias_manager = \Drupal::service('path.alias_manager'); - $alias_manager->cacheClear(); - - $path = '/path/add-one'; - $request = Request::create($path, 'GET'); - $provider->getRouteCollectionForRequest($request); - - $cache = $this->cache->get('route:/path/add-one:'); - $this->assertEqual('/path/add/one', $cache->data['path']); - $this->assertEqual([], $cache->data['query']); - $this->assertEqual(3, count($cache->data['routes'])); - } - - /** - * Test RouteProvider::getRouteByName() and RouteProvider::getRoutesByNames(). - */ - public function testRouteByName() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->sampleRouteCollection()); - $dumper->dump(); - - $route = $provider->getRouteByName('route_a'); - $this->assertEqual($route->getPath(), '/path/one', 'The right route pattern was found.'); - $this->assertEqual($route->getMethods(), ['GET'], 'The right route method was found.'); - $route = $provider->getRouteByName('route_b'); - $this->assertEqual($route->getPath(), '/path/one', 'The right route pattern was found.'); - $this->assertEqual($route->getMethods(), ['PUT'], 'The right route method was found.'); - - $exception_thrown = FALSE; - try { - $provider->getRouteByName('invalid_name'); - } - catch (RouteNotFoundException $e) { - $exception_thrown = TRUE; - } - $this->assertTrue($exception_thrown, 'Random route was not found.'); - - $routes = $provider->getRoutesByNames(array('route_c', 'route_d', $this->randomMachineName())); - $this->assertEqual(count($routes), 2, 'Only two valid routes found.'); - $this->assertEqual($routes['route_c']->getPath(), '/path/two'); - $this->assertEqual($routes['route_d']->getPath(), '/path/three'); - } - - /** - * Ensures that the routing system is capable of extreme long patterns. - */ - public function testGetRoutesByPatternWithLongPatterns() { - $connection = Database::getConnection(); - $provider = new TestRouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - // This pattern has only 3 parts, so we will get candidates, but no routes, - // even though we have not dumped the routes yet. - $shortest = '/test/1/test2'; - $result = $provider->getRoutesByPattern($shortest); - $this->assertEqual($result->count(), 0); - $candidates = $provider->getCandidateOutlines(explode('/', trim($shortest, '/'))); - $this->assertEqual(count($candidates), 7); - // A longer patten is not found and returns no candidates - $path_to_test = '/test/1/test2/2/test3/3/4/5/6/test4'; - $result = $provider->getRoutesByPattern($path_to_test); - $this->assertEqual($result->count(), 0); - $candidates = $provider->getCandidateOutlines(explode('/', trim($path_to_test, '/'))); - $this->assertEqual(count($candidates), 0); - - // Add a matching route and dump it. - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $collection = new RouteCollection(); - $collection->add('long_pattern', new Route('/test/{v1}/test2/{v2}/test3/{v3}/{v4}/{v5}/{v6}/test4')); - $dumper->addRoutes($collection); - $dumper->dump(); - - $result = $provider->getRoutesByPattern($path_to_test); - $this->assertEqual($result->count(), 1); - // We can't compare the values of the routes directly, nor use - // spl_object_hash() because they are separate instances. - $this->assertEqual(serialize($result->get('long_pattern')), serialize($collection->get('long_pattern')), 'The right route was found.'); - // We now have a single candidate outline. - $candidates = $provider->getCandidateOutlines(explode('/', trim($path_to_test, '/'))); - $this->assertEqual(count($candidates), 1); - // Longer and shorter patterns are not found. Both are longer than 3, so - // we should not have any candidates either. The fact that we do not - // get any candidates for a longer path is a security feature. - $longer = '/test/1/test2/2/test3/3/4/5/6/test4/trailing/more/parts'; - $result = $provider->getRoutesByPattern($longer); - $this->assertEqual($result->count(), 0); - $candidates = $provider->getCandidateOutlines(explode('/', trim($longer, '/'))); - $this->assertEqual(count($candidates), 1); - $shorter = '/test/1/test2/2/test3'; - $result = $provider->getRoutesByPattern($shorter); - $this->assertEqual($result->count(), 0); - $candidates = $provider->getCandidateOutlines(explode('/', trim($shorter, '/'))); - $this->assertEqual(count($candidates), 0); - // This pattern has only 3 parts, so we will get candidates, but no routes. - // This result is unchanged by running the dumper. - $result = $provider->getRoutesByPattern($shortest); - $this->assertEqual($result->count(), 0); - $candidates = $provider->getCandidateOutlines(explode('/', trim($shortest, '/'))); - $this->assertEqual(count($candidates), 7); - } - - /** - * Tests getRoutesPaged(). - */ - public function testGetRoutesPaged() { - $connection = Database::getConnection(); - $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); - - $this->fixtures->createTables($connection); - $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); - $dumper->addRoutes($this->fixtures->sampleRouteCollection()); - $dumper->dump(); - - $fixture_routes = $this->fixtures->staticSampleRouteCollection(); - - // Query all the routes. - $routes = $provider->getRoutesPaged(0); - $this->assertEqual(array_keys($routes), array_keys($fixture_routes)); - - // Query non routes. - $routes = $provider->getRoutesPaged(0, 0); - $this->assertEqual(array_keys($routes), []); - - // Query a limited sets of routes. - $routes = $provider->getRoutesPaged(1, 2); - $this->assertEqual(array_keys($routes), array_slice(array_keys($fixture_routes), 1, 2)); - } - -} - -class TestRouteProvider extends RouteProvider { - - public function getCandidateOutlines(array $parts) { - return parent::getCandidateOutlines($parts); - } - -} diff --git a/core/modules/system/src/Tests/Routing/UrlIntegrationTest.php b/core/modules/system/src/Tests/Routing/UrlIntegrationTest.php deleted file mode 100644 index bf927fe..0000000 --- a/core/modules/system/src/Tests/Routing/UrlIntegrationTest.php +++ /dev/null @@ -1,54 +0,0 @@ -rebuild(); - /** @var \Drupal\user\RoleInterface $role_with_access */ - $role_with_access = Role::create(['id' => 'role_with_access']); - $role_with_access->grantPermission('administer users'); - $role_with_access->save(); - - /** @var \Drupal\user\RoleInterface $role_without_access */ - $role_without_access = Role::create(['id' => 'role_without_access']); - $role_without_access->save(); - - $user_with_access = User::create(['roles' => ['role_with_access']]); - $user_without_access = User::create(['roles' => ['role_without_access']]); - - $url_always_access = new Url('router_test.1'); - $this->assertTrue($url_always_access->access($user_with_access)); - $this->assertTrue($url_always_access->access($user_without_access)); - - $url_none_access = new Url('router_test.15'); - $this->assertFalse($url_none_access->access($user_with_access)); - $this->assertFalse($url_none_access->access($user_without_access)); - - $url_access = new Url('router_test.16'); - $this->assertTrue($url_access->access($user_with_access)); - $this->assertFalse($url_access->access($user_without_access)); - } - -} diff --git a/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php b/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php deleted file mode 100644 index 9d1cc72..0000000 --- a/core/modules/system/src/Tests/ServiceProvider/ServiceProviderTest.php +++ /dev/null @@ -1,43 +0,0 @@ -container->getDefinition('file.usage'); - $this->assertTrue($definition->getClass() == 'Drupal\\service_provider_test\\TestFileUsage', 'Class has been changed'); - $this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service has been registered to the DIC'); - } - - /** - * Tests that the DIC keeps up with module enable/disable in the same request. - */ - function testServiceProviderRegistrationDynamic() { - // Uninstall the module and ensure the service provider's service is not registered. - \Drupal::service('module_installer')->uninstall(array('service_provider_test')); - $this->assertFalse(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service does not exist in the DIC.'); - - // Install the module and ensure the service provider's service is registered. - \Drupal::service('module_installer')->install(array('service_provider_test')); - $this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service exists in the DIC.'); - } - -} diff --git a/core/modules/system/src/Tests/Session/AccountSwitcherTest.php b/core/modules/system/src/Tests/Session/AccountSwitcherTest.php deleted file mode 100644 index 7f771f6..0000000 --- a/core/modules/system/src/Tests/Session/AccountSwitcherTest.php +++ /dev/null @@ -1,67 +0,0 @@ -container->get('session_handler.write_safe'); - $user = $this->container->get('current_user'); - $switcher = $this->container->get('account_switcher'); - $original_user = $user->getAccount(); - $original_session_saving = $session_handler->isSessionWritable(); - - // Switch to user with uid 2. - $switcher->switchTo(new UserSession(array('uid' => 2))); - - // Verify that the active user has changed, and that session saving is - // disabled. - $this->assertEqual($user->id(), 2, 'Switched to user 2.'); - $this->assertFalse($session_handler->isSessionWritable(), 'Session saving is disabled.'); - - // Perform a second (nested) user account switch. - $switcher->switchTo(new UserSession(array('uid' => 3))); - $this->assertEqual($user->id(), 3, 'Switched to user 3.'); - - // Revert to the user session that was active between the first and second - // switch. - $switcher->switchBack(); - - // Since we are still in the account from the first switch, session handling - // still needs to be disabled. - $this->assertEqual($user->id(), 2, 'Reverted to user 2.'); - $this->assertFalse($session_handler->isSessionWritable(), 'Session saving still disabled.'); - - // Revert to the original account which was active before the first switch. - $switcher->switchBack(); - - // Assert that the original account is active again, and that session saving - // has been re-enabled. - $this->assertEqual($user->id(), $original_user->id(), 'Original user correctly restored.'); - $this->assertEqual($session_handler->isSessionWritable(), $original_session_saving, 'Original session saving correctly restored.'); - - // Verify that AccountSwitcherInterface::switchBack() will throw - // an exception if there are no accounts left in the stack. - try { - $switcher->switchBack(); - $this->fail('::switchBack() throws exception if called without previous switch.'); - } - catch (\RuntimeException $e) { - if ($e->getMessage() == 'No more accounts to revert to.') { - $this->pass('::switchBack() throws exception if called without previous switch.'); - } - else { - $this->fail($e->getMessage()); - } - } - } - -} diff --git a/core/modules/system/src/Tests/System/DateTimeTest.php b/core/modules/system/src/Tests/System/DateTimeTest.php index f1202cc..9ca050f 100644 --- a/core/modules/system/src/Tests/System/DateTimeTest.php +++ b/core/modules/system/src/Tests/System/DateTimeTest.php @@ -45,7 +45,7 @@ function testTimeZoneHandling() { ->set('timezone.default', 'Pacific/Honolulu') ->set('timezone.user.configurable', 0) ->save(); - entity_load('date_format', 'medium') + DateFormat::load('medium') ->setPattern('Y-m-d H:i:s O') ->save(); @@ -120,7 +120,7 @@ function testDateFormatConfiguration() { $this->assertRaw(t('The date format %format has been deleted.', array('%format' => $name)), 'Custom date format removed.'); // Make sure the date does not exist in config. - $date_format = entity_load('date_format', $date_format_id); + $date_format = DateFormat::load($date_format_id); $this->assertFalse($date_format); // Add a new date format with an existing format. diff --git a/core/modules/system/src/Tests/System/ErrorHandlerTest.php b/core/modules/system/src/Tests/System/ErrorHandlerTest.php index d95c038..113d495 100644 --- a/core/modules/system/src/Tests/System/ErrorHandlerTest.php +++ b/core/modules/system/src/Tests/System/ErrorHandlerTest.php @@ -2,6 +2,7 @@ namespace Drupal\system\Tests\System; +use Drupal\Component\Render\FormattableMarkup; use Drupal\simpletest\WebTestBase; /** @@ -183,7 +184,7 @@ function testExceptionHandler() { * Helper function: assert that the error message is found. */ function assertErrorMessage(array $error) { - $message = t('%type: @message in %function (line ', $error); + $message = new FormattableMarkup('%type: @message in %function (line ', $error); $this->assertRaw($message, format_string('Found error message: @message.', array('@message' => $message))); } @@ -191,7 +192,7 @@ function assertErrorMessage(array $error) { * Helper function: assert that the error message is not found. */ function assertNoErrorMessage(array $error) { - $message = t('%type: @message in %function (line ', $error); + $message = new FormattableMarkup('%type: @message in %function (line ', $error); $this->assertNoRaw($message, format_string('Did not find error message: @message.', array('@message' => $message))); } diff --git a/core/modules/system/src/Tests/System/IgnoreReplicaSubscriberTest.php b/core/modules/system/src/Tests/System/IgnoreReplicaSubscriberTest.php deleted file mode 100644 index 8b35bdf..0000000 --- a/core/modules/system/src/Tests/System/IgnoreReplicaSubscriberTest.php +++ /dev/null @@ -1,43 +0,0 @@ -checkReplicaServer($event); - - $db1 = Database::getConnection('default', 'default'); - $db2 = Database::getConnection('replica', 'default'); - - $this->assertIdentical($db1, $db2, 'System Init ignores secondaries when requested.'); - } - -} diff --git a/core/modules/system/src/Tests/System/InfoAlterTest.php b/core/modules/system/src/Tests/System/InfoAlterTest.php deleted file mode 100644 index b09893b..0000000 --- a/core/modules/system/src/Tests/System/InfoAlterTest.php +++ /dev/null @@ -1,36 +0,0 @@ -set('module_required_test.hook_system_info_alter', TRUE); - $info = system_rebuild_module_data(); - $this->assertFalse(isset($info['node']->info['required']), 'Before the module_required_test is installed the node module is not required.'); - - // Enable the test module. - \Drupal::service('module_installer')->install(array('module_required_test'), FALSE); - $this->assertTrue(\Drupal::moduleHandler()->moduleExists('module_required_test'), 'Test required module is enabled.'); - - $info = system_rebuild_module_data(); - $this->assertTrue($info['node']->info['required'], 'After the module_required_test is installed the node module is required.'); - } - -} diff --git a/core/modules/system/src/Tests/System/SettingsRewriteTest.php b/core/modules/system/src/Tests/System/SettingsRewriteTest.php deleted file mode 100644 index 630db65..0000000 --- a/core/modules/system/src/Tests/System/SettingsRewriteTest.php +++ /dev/null @@ -1,126 +0,0 @@ -container->get('site.path'); - $tests = array( - array( - 'original' => '$no_index_value_scalar = TRUE;', - 'settings' => array( - 'no_index_value_scalar' => (object) array( - 'value' => FALSE, - 'comment' => 'comment', - ), - ), - 'expected' => '$no_index_value_scalar = false; // comment', - ), - array( - 'original' => '$no_index_value_scalar = TRUE;', - 'settings' => array( - 'no_index_value_foo' => array( - 'foo' => array( - 'value' => (object) array( - 'value' => NULL, - 'required' => TRUE, - 'comment' => 'comment', - ), - ), - ), - ), - 'expected' => <<<'EXPECTED' -$no_index_value_scalar = TRUE; -$no_index_value_foo['foo']['value'] = NULL; // comment -EXPECTED - ), - array( - 'original' => '$no_index_value_array = array("old" => "value");', - 'settings' => array( - 'no_index_value_array' => (object) array( - 'value' => FALSE, - 'required' => TRUE, - 'comment' => 'comment', - ), - ), - 'expected' => '$no_index_value_array = array("old" => "value"); -$no_index_value_array = false; // comment', - ), - array( - 'original' => '$has_index_value_scalar["foo"]["bar"] = NULL;', - 'settings' => array( - 'has_index_value_scalar' => array( - 'foo' => array( - 'bar' => (object) array( - 'value' => FALSE, - 'required' => TRUE, - 'comment' => 'comment', - ), - ), - ), - ), - 'expected' => '$has_index_value_scalar["foo"]["bar"] = false; // comment', - ), - array( - 'original' => '$has_index_value_scalar["foo"]["bar"] = "foo";', - 'settings' => array( - 'has_index_value_scalar' => array( - 'foo' => array( - 'value' => (object) array( - 'value' => array('value' => 2), - 'required' => TRUE, - 'comment' => 'comment', - ), - ), - ), - ), - 'expected' => <<<'EXPECTED' -$has_index_value_scalar["foo"]["bar"] = "foo"; -$has_index_value_scalar['foo']['value'] = array ( - 'value' => 2, -); // comment -EXPECTED - ), - ); - foreach ($tests as $test) { - $filename = Settings::get('file_public_path', $site_path . '/files') . '/mock_settings.php'; - file_put_contents(\Drupal::root() . '/' . $filename, "assertEqual(file_get_contents(\Drupal::root() . '/' . $filename), " array( - 'no_index' => (object) array( - 'value' => TRUE, - 'required' => TRUE, - ), - ), - 'expected' => '$no_index = true;' - ); - // Make an empty file. - $filename = Settings::get('file_public_path', $site_path . '/files') . '/mock_settings.php'; - file_put_contents(\Drupal::root() . '/' . $filename, ""); - - // Write the setting to the file. - drupal_rewrite_settings($test['settings'], $filename); - - // Check that the result is just the php opening tag and the settings. - $this->assertEqual(file_get_contents(\Drupal::root() . '/' . $filename), " (object) array( + 'value' => '', + 'required' => TRUE, + ), + ); + $this->writeSettings($settings); + $admin_user = $this->drupalCreateUser(array( 'administer site configuration', )); @@ -60,6 +70,9 @@ public function testStatusPage() { // If a module is fully installed no pending updates exists. $this->assertNoText(t('Out of date')); + // The global $config_directories is not properly formed. + $this->assertRaw(t('Your %file file must define the $config_directories variable as an array containing the names of directories in which configuration files can be found. It must contain a %sync_key key.', array('%file' => $this->siteDirectory . '/settings.php', '%sync_key' => CONFIG_SYNC_DIRECTORY))); + // Set the schema version of update_test_postupdate to a lower version, so // update_test_postupdate_update_8001() needs to be executed. drupal_set_installed_schema_version('update_test_postupdate', 8000); diff --git a/core/modules/system/src/Tests/Theme/ImageTest.php b/core/modules/system/src/Tests/Theme/ImageTest.php deleted file mode 100644 index b5323f4..0000000 --- a/core/modules/system/src/Tests/Theme/ImageTest.php +++ /dev/null @@ -1,145 +0,0 @@ -container = $this->kernel->getContainer(); - $this->container->get('request_stack')->push($request); - - $this->testImages = array( - 'core/misc/druplicon.png', - 'core/misc/loading.gif', - ); - } - - /** - * Tests that an image with the sizes attribute is output correctly. - */ - function testThemeImageWithSizes() { - // Test with multipliers. - $sizes = '(max-width: ' . rand(10, 30) . 'em) 100vw, (max-width: ' . rand(30, 50) . 'em) 50vw, 30vw'; - $image = array( - '#theme' => 'image', - '#sizes' => $sizes, - '#uri' => reset($this->testImages), - '#width' => rand(0, 1000) . 'px', - '#height' => rand(0, 500) . 'px', - '#alt' => $this->randomMachineName(), - '#title' => $this->randomMachineName(), - ); - $this->render($image); - - // Make sure sizes is set. - $this->assertRaw($sizes, 'Sizes is set correctly.'); - } - - /** - * Tests that an image with the src attribute is output correctly. - */ - function testThemeImageWithSrc() { - - $image = array( - '#theme' => 'image', - '#uri' => reset($this->testImages), - '#width' => rand(0, 1000) . 'px', - '#height' => rand(0, 500) . 'px', - '#alt' => $this->randomMachineName(), - '#title' => $this->randomMachineName(), - ); - $this->render($image); - - // Make sure the src attribute has the correct value. - $this->assertRaw(file_url_transform_relative(file_create_url($image['#uri'])), 'Correct output for an image with the src attribute.'); - } - - /** - * Tests that an image with the srcset and multipliers is output correctly. - */ - function testThemeImageWithSrcsetMultiplier() { - // Test with multipliers. - $image = array( - '#theme' => 'image', - '#srcset' => array( - array( - 'uri' => $this->testImages[0], - 'multiplier' => '1x', - ), - array( - 'uri' => $this->testImages[1], - 'multiplier' => '2x', - ), - ), - '#width' => rand(0, 1000) . 'px', - '#height' => rand(0, 500) . 'px', - '#alt' => $this->randomMachineName(), - '#title' => $this->randomMachineName(), - ); - $this->render($image); - - // Make sure the srcset attribute has the correct value. - $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' 1x, ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' 2x', 'Correct output for image with srcset attribute and multipliers.'); - } - - /** - * Tests that an image with the srcset and widths is output correctly. - */ - function testThemeImageWithSrcsetWidth() { - // Test with multipliers. - $widths = array( - rand(0, 500) . 'w', - rand(500, 1000) . 'w', - ); - $image = array( - '#theme' => 'image', - '#srcset' => array( - array( - 'uri' => $this->testImages[0], - 'width' => $widths[0], - ), - array( - 'uri' => $this->testImages[1], - 'width' => $widths[1], - ), - ), - '#width' => rand(0, 1000) . 'px', - '#height' => rand(0, 500) . 'px', - '#alt' => $this->randomMachineName(), - '#title' => $this->randomMachineName(), - ); - $this->render($image); - - // Make sure the srcset attribute has the correct value. - $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' ' . $widths[0] . ', ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' ' . $widths[1], 'Correct output for image with srcset attribute and width descriptors.'); - } - -} diff --git a/core/modules/system/src/Tests/Theme/MessageTest.php b/core/modules/system/src/Tests/Theme/MessageTest.php deleted file mode 100644 index 6605e81..0000000 --- a/core/modules/system/src/Tests/Theme/MessageTest.php +++ /dev/null @@ -1,37 +0,0 @@ -install(['classy']); - \Drupal::service('theme_handler')->setDefault('classy'); - - drupal_set_message('An error occurred', 'error'); - drupal_set_message('But then something nice happened'); - $messages = array( - '#type' => 'status_messages', - ); - $this->render($messages); - $this->assertRaw('messages messages--error'); - $this->assertRaw('messages messages--status'); - } - -} diff --git a/core/modules/system/src/Tests/Theme/RegistryTest.php b/core/modules/system/src/Tests/Theme/RegistryTest.php deleted file mode 100644 index a17d8ed..0000000 --- a/core/modules/system/src/Tests/Theme/RegistryTest.php +++ /dev/null @@ -1,160 +0,0 @@ -setMethod('GET'); - $cid = 'test_theme_registry'; - - // Directly instantiate the theme registry, this will cause a base cache - // entry to be written in __construct(). - $cache = \Drupal::cache(); - $lock_backend = \Drupal::lock(); - $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); - - $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); - - // Trigger a cache miss for an offset. - $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry.'); - // This will cause the ThemeRegistry class to write an updated version of - // the cache entry when it is destroyed, usually at the end of the request. - // Before that happens, manually delete the cache entry we created earlier - // so that the new entry is written from scratch. - \Drupal::cache()->delete($cid); - - // Destroy the class so that it triggers a cache write for the offset. - $registry->destruct(); - - $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); - - // Create a new instance of the class. Confirm that both the offset - // requested previously, and one that has not yet been requested are both - // available. - $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); - $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry'); - $this->assertTrue($registry->get('theme_test_template_test_2'), 'Offset was returned correctly from the theme registry'); - } - - /** - * Tests the theme registry with multiple subthemes. - */ - public function testMultipleSubThemes() { - $theme_handler = \Drupal::service('theme_handler'); - $theme_handler->install(['test_basetheme', 'test_subtheme', 'test_subsubtheme']); - - $registry_subsub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subsubtheme'); - $registry_subsub_theme->setThemeManager(\Drupal::theme()); - $registry_sub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subtheme'); - $registry_sub_theme->setThemeManager(\Drupal::theme()); - $registry_base_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_basetheme'); - $registry_base_theme->setThemeManager(\Drupal::theme()); - - $preprocess_functions = $registry_subsub_theme->get()['theme_test_template_test']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess', - 'test_basetheme_preprocess_theme_test_template_test', - 'test_subtheme_preprocess_theme_test_template_test', - 'test_subsubtheme_preprocess_theme_test_template_test', - ], $preprocess_functions); - - $preprocess_functions = $registry_sub_theme->get()['theme_test_template_test']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess', - 'test_basetheme_preprocess_theme_test_template_test', - 'test_subtheme_preprocess_theme_test_template_test', - ], $preprocess_functions); - - $preprocess_functions = $registry_base_theme->get()['theme_test_template_test']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess', - 'test_basetheme_preprocess_theme_test_template_test', - ], $preprocess_functions); - - $preprocess_functions = $registry_base_theme->get()['theme_test_function_suggestions']['preprocess functions']; - $this->assertIdentical([ - 'template_preprocess_theme_test_function_suggestions', - 'test_basetheme_preprocess_theme_test_function_suggestions', - ], $preprocess_functions, "Theme functions don't have template_preprocess but do have template_preprocess_HOOK"); - } - - /** - * Tests the theme registry with suggestions. - */ - public function testSuggestionPreprocessFunctions() { - $theme_handler = \Drupal::service('theme_handler'); - $theme_handler->install(['test_theme']); - - $registry_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); - $registry_theme->setThemeManager(\Drupal::theme()); - - $suggestions = ['__kitten', '__flamingo']; - $expected_preprocess_functions = [ - 'template_preprocess', - 'theme_test_preprocess_theme_test_preprocess_suggestions', - ]; - $suggestion = ''; - $hook = 'theme_test_preprocess_suggestions'; - do { - $hook .= "$suggestion"; - $expected_preprocess_functions[] = "test_theme_preprocess_$hook"; - $preprocess_functions = $registry_theme->get()[$hook]['preprocess functions']; - $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, "$hook has correct preprocess functions."); - } while ($suggestion = array_shift($suggestions)); - - $expected_preprocess_functions = [ - 'template_preprocess', - 'theme_test_preprocess_theme_test_preprocess_suggestions', - 'test_theme_preprocess_theme_test_preprocess_suggestions', - 'test_theme_preprocess_theme_test_preprocess_suggestions__kitten', - ]; - - $preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat']['preprocess functions']; - $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a function correctly inherits preprocess functions.'); - - $preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__bearcat']['preprocess functions']; - $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a template correctly inherits preprocess functions.'); - - $this->assertTrue(isset($registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat__tarsier__moose']), 'Preprocess function with an unimplemented lower-level suggestion is added to the registry.'); - } - - /** - * Tests that the theme registry can be altered by themes. - */ - public function testThemeRegistryAlterByTheme() { - - /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */ - $theme_handler = \Drupal::service('theme_handler'); - $theme_handler->install(['test_theme']); - $theme_handler->setDefault('test_theme'); - - $registry = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); - $registry->setThemeManager(\Drupal::theme()); - $this->assertEqual('value', $registry->get()['theme_test_template_test']['variables']['additional']); - } - -} diff --git a/core/modules/system/src/Tests/Theme/StableLibraryOverrideTest.php b/core/modules/system/src/Tests/Theme/StableLibraryOverrideTest.php deleted file mode 100644 index d7a6541..0000000 --- a/core/modules/system/src/Tests/Theme/StableLibraryOverrideTest.php +++ /dev/null @@ -1,180 +0,0 @@ -themeManager = $this->container->get('theme.manager'); - $this->themeInitialization = $this->container->get('theme.initialization'); - $this->libraryDiscovery = $this->container->get('library.discovery'); - - $this->container->get('theme_installer')->install(['stable']); - - // Enable all core modules. - $all_modules = system_rebuild_module_data(); - $all_modules = array_filter($all_modules, function ($module) { - // Filter contrib, hidden, already enabled modules and modules in the - // Testing package. - if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') { - return FALSE; - } - return TRUE; - }); - $this->allModules = array_keys($all_modules); - sort($this->allModules); - $this->enableModules($this->allModules); - } - - /** - * Ensures that Stable overrides all relevant core library assets. - */ - public function testStableLibraryOverrides() { - // First get the clean library definitions with no active theme. - $libraries_before = $this->getAllLibraries(); - $libraries_before = $this->removeVendorAssets($libraries_before); - - $this->themeManager->setActiveTheme($this->themeInitialization->getActiveThemeByName('stable')); - $this->libraryDiscovery->clearCachedDefinitions(); - - // Now get the library definitions with Stable as the active theme. - $libraries_after = $this->getAllLibraries(); - $libraries_after = $this->removeVendorAssets($libraries_after); - - $root = \Drupal::root(); - foreach ($libraries_before as $extension => $libraries) { - foreach ($libraries as $library_name => $library) { - // Allow skipping libraries. - if (in_array("$extension/$library_name", $this->librariesToSkip)) { - continue; - } - $library_after = $libraries_after[$extension][$library_name]; - - // Check that all the CSS assets are overridden. - foreach ($library['css'] as $index => $asset) { - $clean_path = $asset['data']; - $stable_path = $library_after['css'][$index]['data']; - // Make core/misc assets look like they are coming from a "core" - // module. - $replacements = [ - 'core/misc/' => "core/modules/core/css/", - ]; - $expected_path = strtr($clean_path, $replacements); - - // Adjust the module asset paths to correspond with the Stable folder - // structure. - $expected_path = str_replace("core/modules/$extension/css/", "core/themes/stable/css/$extension/", $expected_path); - $assert_path = str_replace("core/modules/$extension/", '', $clean_path); - - $this->assertEqual($expected_path, $stable_path, "$assert_path from the $extension/$library_name library is overridden in Stable."); - } - } - } - } - - /** - * Removes all vendor libraries and assets from the library definitions. - * - * @param array[] $all_libraries - * An associative array of libraries keyed by extension, then by library - * name, and so on. - * - * @return array[] - * The reduced array of libraries. - */ - protected function removeVendorAssets($all_libraries) { - foreach ($all_libraries as $extension => $libraries) { - foreach ($libraries as $library_name => $library) { - if (isset($library['remote'])) { - unset($all_libraries[$extension][$library_name]); - } - foreach (['css', 'js'] as $asset_type) { - foreach ($library[$asset_type] as $index => $asset) { - if (strpos($asset['data'], 'core/assets/vendor') !== FALSE) { - unset($all_libraries[$extension][$library_name][$asset_type][$index]); - // Re-key the array of assets. This is needed because - // libraries-override doesn't always preserve the order. - if (!empty($all_libraries[$extension][$library_name][$asset_type])) { - $all_libraries[$extension][$library_name][$asset_type] = array_values($all_libraries[$extension][$library_name][$asset_type]); - } - } - } - } - } - } - return $all_libraries; - } - - /** - * Gets all libraries for core and all installed modules. - * - * @return array[] - * An associative array of libraries keyed by extension, then by library - * name, and so on. - */ - protected function getAllLibraries() { - $modules = \Drupal::moduleHandler()->getModuleList(); - $module_list = array_keys($modules); - sort($module_list); - $this->assertEqual($this->allModules, $module_list, 'All core modules are installed.'); - - $libraries['core'] = $this->libraryDiscovery->getLibrariesByExtension('core'); - - $root = \Drupal::root(); - foreach ($modules as $module_name => $module) { - $library_file = $module->getPath() . '/' . $module_name . '.libraries.yml'; - if (is_file($root . '/' . $library_file)) { - $libraries[$module_name] = $this->libraryDiscovery->getLibrariesByExtension($module_name); - } - } - return $libraries; - } - -} diff --git a/core/modules/system/src/Tests/Theme/StableThemeTest.php b/core/modules/system/src/Tests/Theme/StableThemeTest.php deleted file mode 100644 index 1290847..0000000 --- a/core/modules/system/src/Tests/Theme/StableThemeTest.php +++ /dev/null @@ -1,70 +0,0 @@ -themeHandler = $this->container->get('theme_handler'); - $this->themeManager = $this->container->get('theme.manager'); - } - - /** - * Ensures Stable is used by default when no base theme has been defined. - */ - public function testStableIsDefault() { - $this->themeHandler->install(['test_stable']); - $this->themeHandler->setDefault('test_stable'); - $theme = $this->themeManager->getActiveTheme(); - /** @var \Drupal\Core\Theme\ActiveTheme $base_theme */ - $base_themes = $theme->getBaseThemes(); - $base_theme = reset($base_themes); - $this->assertTrue($base_theme->getName() == 'stable', "Stable theme is the base theme if a theme hasn't decided to opt out."); - } - - /** - * Tests opting out of Stable by setting the base theme to false. - */ - public function testWildWest() { - $this->themeHandler->install(['test_wild_west']); - $this->themeHandler->setDefault('test_wild_west'); - $theme = $this->themeManager->getActiveTheme(); - /** @var \Drupal\Core\Theme\ActiveTheme $base_theme */ - $base_themes = $theme->getBaseThemes(); - $this->assertTrue(empty($base_themes), 'No base theme is set when a theme has opted out of using Stable.'); - } - -} diff --git a/core/modules/system/src/Tests/Theme/ThemeSettingsTest.php b/core/modules/system/src/Tests/Theme/ThemeSettingsTest.php deleted file mode 100644 index b7e6866..0000000 --- a/core/modules/system/src/Tests/Theme/ThemeSettingsTest.php +++ /dev/null @@ -1,63 +0,0 @@ -installConfig(array('system')); - - if (!isset($this->availableThemes)) { - $discovery = new ExtensionDiscovery(\Drupal::root()); - $this->availableThemes = $discovery->scan('theme'); - } - } - - /** - * Tests that $theme.settings are imported and used as default theme settings. - */ - function testDefaultConfig() { - $name = 'test_basetheme'; - $path = $this->availableThemes[$name]->getPath(); - $this->assertTrue(file_exists("$path/" . InstallStorage::CONFIG_INSTALL_DIRECTORY . "/$name.settings.yml")); - $this->container->get('theme_handler')->install(array($name)); - $this->assertIdentical(theme_get_setting('base', $name), 'only'); - } - - /** - * Tests that the $theme.settings default config file is optional. - */ - function testNoDefaultConfig() { - $name = 'stark'; - $path = $this->availableThemes[$name]->getPath(); - $this->assertFalse(file_exists("$path/" . InstallStorage::CONFIG_INSTALL_DIRECTORY . "/$name.settings.yml")); - $this->container->get('theme_handler')->install(array($name)); - $this->assertNotNull(theme_get_setting('features.favicon', $name)); - } - -} diff --git a/core/modules/system/src/Tests/Theme/TwigEnvironmentTest.php b/core/modules/system/src/Tests/Theme/TwigEnvironmentTest.php deleted file mode 100644 index ef3c4697..0000000 --- a/core/modules/system/src/Tests/Theme/TwigEnvironmentTest.php +++ /dev/null @@ -1,130 +0,0 @@ -container->get('renderer'); - /** @var \Drupal\Core\Template\TwigEnvironment $environment */ - $environment = \Drupal::service('twig'); - $this->assertEqual($environment->renderInline('test-no-context'), 'test-no-context'); - $this->assertEqual($environment->renderInline('test-with-context {{ llama }}', array('llama' => 'muuh')), 'test-with-context muuh'); - - $element = array(); - $unsafe_string = ''; - $element['test'] = array( - '#type' => 'inline_template', - '#template' => 'test-with-context {{ unsafe_content }}', - '#context' => array('unsafe_content' => $unsafe_string), - ); - $this->assertEqual($renderer->renderRoot($element), 'test-with-context ' . Html::escape($unsafe_string) . ''); - - // Enable twig_auto_reload and twig_debug. - $settings = Settings::getAll(); - $settings['twig_debug'] = TRUE; - $settings['twig_auto_reload'] = TRUE; - - new Settings($settings); - $this->container = $this->kernel->rebuildContainer(); - \Drupal::setContainer($this->container); - - $element = array(); - $element['test'] = array( - '#type' => 'inline_template', - '#template' => 'test-with-context {{ llama }}', - '#context' => array('llama' => 'muuh'), - ); - $element_copy = $element; - // Render it twice so that twig caching is triggered. - $this->assertEqual($renderer->renderRoot($element), 'test-with-context muuh'); - $this->assertEqual($renderer->renderRoot($element_copy), 'test-with-context muuh'); - - // Tests caching of inline templates with long content to ensure the - // generated cache key can be used as a filename. - $element = []; - $element['test'] = [ - '#type' => 'inline_template', - '#template' => 'Llamas sometimes spit and wrestle with their {{ llama }}. Kittens are soft and fuzzy and they sometimes say {{ kitten }}. Flamingos have long legs and they are usually {{ flamingo }}. Pandas eat bamboo and they are {{ panda }}. Giraffes have long necks and long tongues and they eat {{ giraffe }}.', - '#context' => [ - 'llama' => 'necks', - 'kitten' => 'meow', - 'flamingo' => 'pink', - 'panda' => 'bears', - 'giraffe' => 'leaves', - ], - ]; - $expected = 'Llamas sometimes spit and wrestle with their necks. Kittens are soft and fuzzy and they sometimes say meow. Flamingos have long legs and they are usually pink. Pandas eat bamboo and they are bears. Giraffes have long necks and long tongues and they eat leaves.'; - $element_copy = $element; - - // Render it twice so that twig caching is triggered. - $this->assertEqual($renderer->renderRoot($element), $expected); - $this->assertEqual($renderer->renderRoot($element_copy), $expected); - - $name = '{# inline_template_start #}' . $element['test']['#template']; - $hash = $this->container->getParameter('twig_extension_hash'); - - $cache = $environment->getCache(); - $class = $environment->getTemplateClass($name); - $expected = $hash . '_inline-template' . '_' . hash('sha256', $class); - $this->assertEqual($expected, $cache->generateKey($name, $class)); - } - - /** - * Tests that exceptions are thrown when a template is not found. - */ - public function testTemplateNotFoundException() { - /** @var \Drupal\Core\Template\TwigEnvironment $environment */ - $environment = \Drupal::service('twig'); - - try { - $environment->loadTemplate('this-template-does-not-exist.html.twig')->render(array()); - $this->fail('Did not throw an exception as expected.'); - } - catch (\Twig_Error_Loader $e) { - $this->assertTrue(strpos($e->getMessage(), 'Template "this-template-does-not-exist.html.twig" is not defined') === 0); - } - } - - /** - * Ensures that cacheFilename() varies by extensions + deployment identifier. - */ - public function testCacheFilename() { - /** @var \Drupal\Core\Template\TwigEnvironment $environment */ - // Note: Later we refetch the twig service in order to bypass its internal - // static cache. - $environment = \Drupal::service('twig'); - - $original_filename = $environment->getCacheFilename('core/modules/system/templates/container.html.twig'); - \Drupal::getContainer()->set('twig', NULL); - - \Drupal::service('module_installer')->install(['twig_extension_test']); - $environment = \Drupal::service('twig'); - $new_extension_filename = $environment->getCacheFilename('core/modules/system/templates/container.html.twig'); - \Drupal::getContainer()->set('twig', NULL); - - $this->assertNotEqual($new_extension_filename, $original_filename); - } - -} diff --git a/core/modules/system/src/Tests/Theme/TwigWhiteListTest.php b/core/modules/system/src/Tests/Theme/TwigWhiteListTest.php deleted file mode 100644 index 7f644fe..0000000 --- a/core/modules/system/src/Tests/Theme/TwigWhiteListTest.php +++ /dev/null @@ -1,126 +0,0 @@ -installSchema('system', array('sequences')); - $this->installEntitySchema('node'); - $this->installEntitySchema('user'); - $this->installEntitySchema('taxonomy_term'); - NodeType::create([ - 'type' => 'page', - 'name' => 'Basic page', - 'display_submitted' => FALSE, - ])->save(); - // Add a vocabulary so we can test different view modes. - $vocabulary = Vocabulary::create([ - 'name' => $this->randomMachineName(), - 'description' => $this->randomMachineName(), - 'vid' => $this->randomMachineName(), - 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - 'help' => '', - ]); - $vocabulary->save(); - - // Add a term to the vocabulary. - $this->term = Term::create([ - 'name' => 'Sometimes people are just jerks', - 'description' => $this->randomMachineName(), - 'vid' => $vocabulary->id(), - 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ]); - $this->term->save(); - - // Create a field. - $handler_settings = array( - 'target_bundles' => array( - $vocabulary->id() => $vocabulary->id(), - ), - 'auto_create' => TRUE, - ); - // Add the term field. - FieldStorageConfig::create(array( - 'field_name' => 'field_term', - 'type' => 'entity_reference', - 'entity_type' => 'node', - 'cardinality' => 1, - 'settings' => array( - 'target_type' => 'taxonomy_term', - ), - ))->save(); - FieldConfig::create(array( - 'field_name' => 'field_term', - 'entity_type' => 'node', - 'bundle' => 'page', - 'label' => 'Terms', - 'settings' => array( - 'handler' => 'default', - 'handler_settings' => $handler_settings, - ), - ))->save(); - - // Show on default display and teaser. - entity_get_display('node', 'page', 'default') - ->setComponent('field_term', array( - 'type' => 'entity_reference_label', - )) - ->save(); - // Boot twig environment. - $this->twig = \Drupal::service('twig'); - } - - /** - * Tests white-listing of methods doesn't interfere with chaining. - */ - public function testWhiteListChaining() { - $node = Node::create([ - 'type' => 'page', - 'title' => 'Some node mmk', - 'status' => 1, - 'field_term' => $this->term->id(), - ]); - $node->save(); - $this->setRawContent(twig_render_template(drupal_get_path('theme', 'test_theme') . '/templates/node.html.twig', ['node' => $node])); - $this->assertText('Sometimes people are just jerks'); - } - -} diff --git a/core/modules/system/src/Tests/TypedData/TypedDataDefinitionTest.php b/core/modules/system/src/Tests/TypedData/TypedDataDefinitionTest.php deleted file mode 100644 index 034987b..0000000 --- a/core/modules/system/src/Tests/TypedData/TypedDataDefinitionTest.php +++ /dev/null @@ -1,99 +0,0 @@ -typedDataManager = $this->container->get('typed_data_manager'); - } - - /** - * Tests deriving metadata about list items. - */ - public function testLists() { - $list_definition = ListDataDefinition::create('string'); - $this->assertTrue($list_definition instanceof ListDataDefinitionInterface); - $item_definition = $list_definition->getItemDefinition(); - $this->assertTrue($item_definition instanceof DataDefinitionInterface); - $this->assertEqual($item_definition->getDataType(), 'string'); - - // Test using the definition factory. - $list_definition2 = $this->typedDataManager->createListDataDefinition('string'); - $this->assertTrue($list_definition2 instanceof ListDataDefinitionInterface); - $this->assertEqual($list_definition, $list_definition2); - - // Test creating the definition of data with type 'list', which is the same - // as creating a definition of a list of items of type 'any'. - $list_definition = $this->typedDataManager->createDataDefinition('list'); - $this->assertTrue($list_definition instanceof ListDataDefinitionInterface); - $this->assertEqual($list_definition->getDataType(), 'list'); - $this->assertEqual($list_definition->getItemDefinition()->getDataType(), 'any'); - } - - /** - * Tests deriving metadata about maps. - */ - public function testMaps() { - $map_definition = MapDataDefinition::create() - ->setPropertyDefinition('one', DataDefinition::create('string')) - ->setPropertyDefinition('two', DataDefinition::create('string')) - ->setPropertyDefinition('three', DataDefinition::create('string')); - - $this->assertTrue($map_definition instanceof ComplexDataDefinitionInterface); - - // Test retrieving metadata about contained properties. - $this->assertEqual(array_keys($map_definition->getPropertyDefinitions()), array('one', 'two', 'three')); - $this->assertEqual($map_definition->getPropertyDefinition('one')->getDataType(), 'string'); - $this->assertNull($map_definition->getMainPropertyName()); - $this->assertNull($map_definition->getPropertyDefinition('invalid')); - - // Test using the definition factory. - $map_definition2 = $this->typedDataManager->createDataDefinition('map'); - $this->assertTrue($map_definition2 instanceof ComplexDataDefinitionInterface); - $map_definition2->setPropertyDefinition('one', DataDefinition::create('string')) - ->setPropertyDefinition('two', DataDefinition::create('string')) - ->setPropertyDefinition('three', DataDefinition::create('string')); - $this->assertEqual($map_definition, $map_definition2); - } - - /** - * Tests deriving metadata from data references. - */ - public function testDataReferences() { - $language_reference_definition = DataReferenceDefinition::create('language'); - $this->assertTrue($language_reference_definition instanceof DataReferenceDefinitionInterface); - - // Test retrieving metadata about the referenced data. - $this->assertEqual($language_reference_definition->getTargetDefinition()->getDataType(), 'language'); - - // Test using the definition factory. - $language_reference_definition2 = $this->typedDataManager->createDataDefinition('language_reference'); - $this->assertTrue($language_reference_definition2 instanceof DataReferenceDefinitionInterface); - $this->assertEqual($language_reference_definition, $language_reference_definition2); - } - -} diff --git a/core/modules/system/src/Tests/TypedData/TypedDataTest.php b/core/modules/system/src/Tests/TypedData/TypedDataTest.php deleted file mode 100644 index 8473485..0000000 --- a/core/modules/system/src/Tests/TypedData/TypedDataTest.php +++ /dev/null @@ -1,634 +0,0 @@ -installEntitySchema('file'); - $this->typedDataManager = $this->container->get('typed_data_manager'); - } - - /** - * Creates a typed data object and ensures it implements TypedDataInterface. - * - * @see \Drupal\Core\TypedData\TypedDataManager::create() - */ - protected function createTypedData($definition, $value = NULL, $name = NULL) { - if (is_array($definition)) { - $definition = DataDefinition::create($definition['type']); - } - $data = $this->typedDataManager->create($definition, $value, $name); - $this->assertTrue($data instanceof TypedDataInterface, 'Typed data object is an instance of the typed data interface.'); - return $data; - } - - /** - * Tests the basics around constructing and working with typed data objects. - */ - public function testGetAndSet() { - // Boolean type. - $typed_data = $this->createTypedData(array('type' => 'boolean'), TRUE); - $this->assertTrue($typed_data instanceof BooleanInterface, 'Typed data object is an instance of BooleanInterface.'); - $this->assertTrue($typed_data->getValue() === TRUE, 'Boolean value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(FALSE); - $this->assertTrue($typed_data->getValue() === FALSE, 'Boolean value was changed.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $this->assertTrue(is_string($typed_data->getString()), 'Boolean value was converted to string'); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Boolean wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - - // String type. - $value = $this->randomString(); - $typed_data = $this->createTypedData(array('type' => 'string'), $value); - $this->assertTrue($typed_data instanceof StringInterface, 'Typed data object is an instance of StringInterface.'); - $this->assertTrue($typed_data->getValue() === $value, 'String value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $new_value = $this->randomString(); - $typed_data->setValue($new_value); - $this->assertTrue($typed_data->getValue() === $new_value, 'String value was changed.'); - $this->assertEqual($typed_data->validate()->count(), 0); - // Funky test. - $this->assertTrue(is_string($typed_data->getString()), 'String value was converted to string'); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'String wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(array('no string')); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - - // Integer type. - $value = rand(); - $typed_data = $this->createTypedData(array('type' => 'integer'), $value); - $this->assertTrue($typed_data instanceof IntegerInterface, 'Typed data object is an instance of IntegerInterface.'); - $this->assertTrue($typed_data->getValue() === $value, 'Integer value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $new_value = rand(); - $typed_data->setValue($new_value); - $this->assertTrue($typed_data->getValue() === $new_value, 'Integer value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'Integer value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Integer wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - - // Float type. - $value = 123.45; - $typed_data = $this->createTypedData(array('type' => 'float'), $value); - $this->assertTrue($typed_data instanceof FloatInterface, 'Typed data object is an instance of FloatInterface.'); - $this->assertTrue($typed_data->getValue() === $value, 'Float value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $new_value = 678.90; - $typed_data->setValue($new_value); - $this->assertTrue($typed_data->getValue() === $new_value, 'Float value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'Float value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Float wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - - // Date Time type. - $value = '2014-01-01T20:00:00+00:00'; - $typed_data = $this->createTypedData(array('type' => 'datetime_iso8601'), $value); - $this->assertTrue($typed_data instanceof DateTimeInterface, 'Typed data object is an instance of DateTimeInterface.'); - $this->assertTrue($typed_data->getValue() == $value, 'Date value was fetched.'); - $this->assertEqual($typed_data->getValue(), $typed_data->getDateTime()->format('c'), 'Value representation of a date is ISO 8601'); - $this->assertEqual($typed_data->validate()->count(), 0); - $new_value = '2014-01-02T20:00:00+00:00'; - $typed_data->setValue($new_value); - $this->assertTrue($typed_data->getDateTime()->format('c') === $new_value, 'Date value was changed and set by an ISO8601 date.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $this->assertTrue($typed_data->getDateTime()->format('Y-m-d') == '2014-01-02', 'Date value was changed and set by date string.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getDateTime(), 'Date wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - // Check implementation of DateTimeInterface. - $typed_data = $this->createTypedData(array('type' => 'datetime_iso8601'), '2014-01-01T20:00:00+00:00'); - $this->assertTrue($typed_data->getDateTime() instanceof DrupalDateTime); - $typed_data->setDateTime(new DrupalDateTime('2014-01-02T20:00:00+00:00')); - $this->assertEqual($typed_data->getValue(), '2014-01-02T20:00:00+00:00'); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getDateTime()); - - // Timestamp type. - $value = REQUEST_TIME; - $typed_data = $this->createTypedData(array('type' => 'timestamp'), $value); - $this->assertTrue($typed_data instanceof DateTimeInterface, 'Typed data object is an instance of DateTimeInterface.'); - $this->assertTrue($typed_data->getValue() == $value, 'Timestamp value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $new_value = REQUEST_TIME + 1; - $typed_data->setValue($new_value); - $this->assertTrue($typed_data->getValue() === $new_value, 'Timestamp value was changed and set.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getDateTime(), 'Timestamp wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - // Check implementation of DateTimeInterface. - $typed_data = $this->createTypedData(array('type' => 'timestamp'), REQUEST_TIME); - $this->assertTrue($typed_data->getDateTime() instanceof DrupalDateTime); - $typed_data->setDateTime(DrupalDateTime::createFromTimestamp(REQUEST_TIME + 1)); - $this->assertEqual($typed_data->getValue(), REQUEST_TIME + 1); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getDateTime()); - - // DurationIso8601 type. - $value = 'PT20S'; - $typed_data = $this->createTypedData(array('type' => 'duration_iso8601'), $value); - $this->assertTrue($typed_data instanceof DurationInterface, 'Typed data object is an instance of DurationInterface.'); - $this->assertIdentical($typed_data->getValue(), $value, 'DurationIso8601 value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('P40D'); - $this->assertEqual($typed_data->getDuration()->d, 40, 'DurationIso8601 value was changed and set by duration string.'); - $this->assertTrue(is_string($typed_data->getString()), 'DurationIso8601 value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'DurationIso8601 wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - // Check implementation of DurationInterface. - $typed_data = $this->createTypedData(array('type' => 'duration_iso8601'), 'PT20S'); - $this->assertTrue($typed_data->getDuration() instanceof \DateInterval); - $typed_data->setDuration(new \DateInterval('P40D')); - // @todo: Should we make this "nicer"? - $this->assertEqual($typed_data->getValue(), 'P0Y0M40DT0H0M0S'); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getDuration()); - - // Time span type. - $value = 20; - $typed_data = $this->createTypedData(array('type' => 'timespan'), $value); - $this->assertTrue($typed_data instanceof DurationInterface, 'Typed data object is an instance of DurationInterface.'); - $this->assertIdentical($typed_data->getValue(), $value, 'Time span value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(60 * 60 * 4); - $this->assertEqual($typed_data->getDuration()->s, 14400, 'Time span was changed'); - $this->assertTrue(is_string($typed_data->getString()), 'Time span value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Time span wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - // Check implementation of DurationInterface. - $typed_data = $this->createTypedData(array('type' => 'timespan'), 20); - $this->assertTrue($typed_data->getDuration() instanceof \DateInterval); - $typed_data->setDuration(new \DateInterval('PT4H')); - $this->assertEqual($typed_data->getValue(), 60 * 60 * 4); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getDuration()); - - // URI type. - $uri = 'http://example.com/foo/'; - $typed_data = $this->createTypedData(array('type' => 'uri'), $uri); - $this->assertTrue($typed_data instanceof UriInterface, 'Typed data object is an instance of UriInterface.'); - $this->assertTrue($typed_data->getValue() === $uri, 'URI value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue($uri . 'bar.txt'); - $this->assertTrue($typed_data->getValue() === $uri . 'bar.txt', 'URI value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'URI value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'URI wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - $typed_data->setValue('public://field/image/Photo on 4-28-14 at 12.01 PM.jpg'); - $this->assertEqual($typed_data->validate()->count(), 0, 'Filename with spaces is valid.'); - - // Generate some files that will be used to test the binary data type. - $files = array(); - for ($i = 0; $i < 3; $i++) { - $path = "public://example_$i.png"; - file_unmanaged_copy(\Drupal::root() . '/core/misc/druplicon.png', $path); - $image = File::create(['uri' => $path]); - $image->save(); - $files[] = $image; - } - - // Email type. - $value = $this->randomString(); - $typed_data = $this->createTypedData(array('type' => 'email'), $value); - $this->assertTrue($typed_data instanceof StringInterface, 'Typed data object is an instance of StringInterface.'); - $this->assertIdentical($typed_data->getValue(), $value, 'Email value was fetched.'); - $new_value = 'test@example.com'; - $typed_data->setValue($new_value); - $this->assertIdentical($typed_data->getValue(), $new_value, 'Email value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'Email value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Email wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalidATexample.com'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - - // Binary type. - $typed_data = $this->createTypedData(array('type' => 'binary'), $files[0]->getFileUri()); - $this->assertTrue($typed_data instanceof BinaryInterface, 'Typed data object is an instance of BinaryInterface.'); - $this->assertTrue(is_resource($typed_data->getValue()), 'Binary value was fetched.'); - $this->assertEqual($typed_data->validate()->count(), 0); - // Try setting by URI. - $typed_data->setValue($files[1]->getFileUri()); - $this->assertEqual(fgets($typed_data->getValue()), fgets(fopen($files[1]->getFileUri(), 'r')), 'Binary value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'Binary value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - // Try setting by resource. - $typed_data->setValue(fopen($files[2]->getFileUri(), 'r')); - $this->assertEqual(fgets($typed_data->getValue()), fgets(fopen($files[2]->getFileUri(), 'r')), 'Binary value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'Binary value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Binary wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue('invalid'); - $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); - - // Any type. - $value = array('foo'); - $typed_data = $this->createTypedData(array('type' => 'any'), $value); - $this->assertIdentical($typed_data->getValue(), $value, 'Any value was fetched.'); - $new_value = 'test@example.com'; - $typed_data->setValue($new_value); - $this->assertIdentical($typed_data->getValue(), $new_value, 'Any value was changed.'); - $this->assertTrue(is_string($typed_data->getString()), 'Any value was converted to string'); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue(), 'Any wrapper is null-able.'); - $this->assertEqual($typed_data->validate()->count(), 0); - // We cannot test invalid values as everything is valid for the any type, - // but make sure an array or object value passes validation also. - $typed_data->setValue(array('entry')); - $this->assertEqual($typed_data->validate()->count(), 0); - $typed_data->setValue((object) array('entry')); - $this->assertEqual($typed_data->validate()->count(), 0); - } - - /** - * Tests using typed data lists. - */ - public function testTypedDataLists() { - // Test working with an existing list of strings. - $value = array('one', 'two', 'three'); - $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); - $this->assertEqual($typed_data->getValue(), $value, 'List value has been set.'); - // Test iterating. - $count = 0; - foreach ($typed_data as $item) { - $this->assertTrue($item instanceof TypedDataInterface); - $count++; - } - $this->assertEqual($count, 3); - - // Test getting the string representation. - $this->assertEqual($typed_data->getString(), 'one, two, three'); - $typed_data[1] = ''; - $this->assertEqual($typed_data->getString(), 'one, three'); - - // Test using array access. - $this->assertEqual($typed_data[0]->getValue(), 'one'); - $typed_data[] = 'four'; - $this->assertEqual($typed_data[3]->getValue(), 'four'); - $this->assertEqual($typed_data->count(), 4); - $this->assertTrue(isset($typed_data[0])); - $this->assertTrue(!isset($typed_data[6])); - - // Test isEmpty and cloning. - $this->assertFalse($typed_data->isEmpty()); - $clone = clone $typed_data; - $this->assertTrue($typed_data->getValue() === $clone->getValue()); - $this->assertTrue($typed_data[0] !== $clone[0]); - $clone->setValue(array()); - $this->assertTrue($clone->isEmpty()); - - // Make sure that resetting the value using NULL results in an empty array. - $clone->setValue(array()); - $typed_data->setValue(NULL); - $this->assertIdentical($typed_data->getValue(), array()); - $this->assertIdentical($clone->getValue(), array()); - - // Test dealing with NULL items. - $typed_data[] = NULL; - $this->assertTrue($typed_data->isEmpty()); - $this->assertEqual(count($typed_data), 1); - $typed_data[] = ''; - $this->assertFalse($typed_data->isEmpty()); - $this->assertEqual(count($typed_data), 2); - $typed_data[] = 'three'; - $this->assertFalse($typed_data->isEmpty()); - $this->assertEqual(count($typed_data), 3); - - $this->assertEqual($typed_data->getValue(), array(NULL, '', 'three')); - // Test unsetting. - unset($typed_data[1]); - $this->assertEqual(count($typed_data), 2); - // Check that items were shifted. - $this->assertEqual($typed_data[1]->getValue(), 'three'); - - // Getting a not set list item returns NULL, and does not create a new item. - $this->assertNull($typed_data[2]); - $this->assertEqual(count($typed_data), 2); - - // Test setting the list with less values. - $typed_data->setValue(array('one')); - $this->assertEqual($typed_data->count(), 1); - - // Test setting invalid values. - try { - $typed_data->setValue('string'); - $this->fail('No exception has been thrown when setting an invalid value.'); - } - catch (\Exception $e) { - $this->pass('Exception thrown:' . $e->getMessage()); - } - } - - /** - * Tests the filter() method on typed data lists. - */ - public function testTypedDataListsFilter() { - // Check that an all-pass filter leaves the list untouched. - $value = array('zero', 'one'); - $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); - $typed_data->filter(function(TypedDataInterface $item) { - return TRUE; - }); - $this->assertEqual($typed_data->count(), 2); - $this->assertEqual($typed_data[0]->getValue(), 'zero'); - $this->assertEqual($typed_data[0]->getName(), 0); - $this->assertEqual($typed_data[1]->getValue(), 'one'); - $this->assertEqual($typed_data[1]->getName(), 1); - - // Check that a none-pass filter empties the list. - $value = array('zero', 'one'); - $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); - $typed_data->filter(function(TypedDataInterface $item) { - return FALSE; - }); - $this->assertEqual($typed_data->count(), 0); - - // Check that filtering correctly renumbers elements. - $value = array('zero', 'one', 'two'); - $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); - $typed_data->filter(function(TypedDataInterface $item) { - return $item->getValue() !== 'one'; - }); - $this->assertEqual($typed_data->count(), 2); - $this->assertEqual($typed_data[0]->getValue(), 'zero'); - $this->assertEqual($typed_data[0]->getName(), 0); - $this->assertEqual($typed_data[1]->getValue(), 'two'); - $this->assertEqual($typed_data[1]->getName(), 1); - } - - /** - * Tests using a typed data map. - */ - public function testTypedDataMaps() { - // Test working with a simple map. - $value = array( - 'one' => 'eins', - 'two' => 'zwei', - 'three' => 'drei', - ); - $definition = MapDataDefinition::create() - ->setPropertyDefinition('one', DataDefinition::create('string')) - ->setPropertyDefinition('two', DataDefinition::create('string')) - ->setPropertyDefinition('three', DataDefinition::create('string')); - - $typed_data = $this->createTypedData($definition, $value); - - // Test iterating. - $count = 0; - foreach ($typed_data as $item) { - $this->assertTrue($item instanceof TypedDataInterface); - $count++; - } - $this->assertEqual($count, 3); - - // Test retrieving metadata. - $this->assertEqual(array_keys($typed_data->getDataDefinition()->getPropertyDefinitions()), array_keys($value)); - $definition = $typed_data->getDataDefinition()->getPropertyDefinition('one'); - $this->assertEqual($definition->getDataType(), 'string'); - $this->assertNull($typed_data->getDataDefinition()->getPropertyDefinition('invalid')); - - // Test getting and setting properties. - $this->assertEqual($typed_data->get('one')->getValue(), 'eins'); - $this->assertEqual($typed_data->toArray(), $value); - $typed_data->set('one', 'uno'); - $this->assertEqual($typed_data->get('one')->getValue(), 'uno'); - // Make sure the update is reflected in the value of the map also. - $value = $typed_data->getValue(); - $this->assertEqual($value, array( - 'one' => 'uno', - 'two' => 'zwei', - 'three' => 'drei' - )); - - $properties = $typed_data->getProperties(); - $this->assertEqual(array_keys($properties), array_keys($value)); - $this->assertIdentical($properties['one'], $typed_data->get('one'), 'Properties are identical.'); - - // Test setting a not defined property. It shouldn't show up in the - // properties, but be kept in the values. - $typed_data->setValue(array('foo' => 'bar')); - $this->assertEqual(array_keys($typed_data->getProperties()), array('one', 'two', 'three')); - $this->assertEqual(array_keys($typed_data->getValue()), array('foo', 'one', 'two', 'three')); - - // Test getting the string representation. - $typed_data->setValue(array('one' => 'eins', 'two' => '', 'three' => 'drei')); - $this->assertEqual($typed_data->getString(), 'eins, drei'); - - // Test isEmpty and cloning. - $this->assertFalse($typed_data->isEmpty()); - $clone = clone $typed_data; - $this->assertTrue($typed_data->getValue() === $clone->getValue()); - $this->assertTrue($typed_data->get('one') !== $clone->get('one')); - $clone->setValue(array()); - $this->assertTrue($clone->isEmpty()); - - // Make sure the difference between NULL (not set) and an empty array is - // kept. - $typed_data->setValue(NULL); - $this->assertNull($typed_data->getValue()); - $typed_data->setValue(array()); - $value = $typed_data->getValue(); - $this->assertTrue(isset($value) && is_array($value)); - - // Test accessing invalid properties. - $typed_data->setValue($value); - try { - $typed_data->get('invalid'); - $this->fail('No exception has been thrown when getting an invalid value.'); - } - catch (\Exception $e) { - $this->pass('Exception thrown:' . $e->getMessage()); - } - - // Test setting invalid values. - try { - $typed_data->setValue('invalid'); - $this->fail('No exception has been thrown when setting an invalid value.'); - } - catch (\Exception $e) { - $this->pass('Exception thrown:' . $e->getMessage()); - } - - // Test adding a new property to the map. - $typed_data->getDataDefinition()->setPropertyDefinition('zero', DataDefinition::create('any')); - $typed_data->set('zero', 'null'); - $this->assertEqual($typed_data->get('zero')->getValue(), 'null'); - $definition = $typed_data->get('zero')->getDataDefinition(); - $this->assertEqual($definition->getDataType(), 'any', 'Definition for a new map entry returned.'); - } - - /** - * Tests typed data validation. - */ - public function testTypedDataValidation() { - $definition = DataDefinition::create('integer') - ->setConstraints(array( - 'Range' => array('min' => 5), - )); - $violations = $this->typedDataManager->create($definition, 10)->validate(); - $this->assertEqual($violations->count(), 0); - - $integer = $this->typedDataManager->create($definition, 1); - $violations = $integer->validate(); - $this->assertEqual($violations->count(), 1); - - // Test translating violation messages. - $message = t('This value should be %limit or more.', array('%limit' => 5)); - $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); - $this->assertEqual($violations[0]->getPropertyPath(), ''); - $this->assertIdentical($violations[0]->getRoot(), $integer, 'Root object returned.'); - - // Test translating violation messages when pluralization is used. - $definition = DataDefinition::create('string') - ->setConstraints(array( - 'Length' => array('min' => 10), - )); - $violations = $this->typedDataManager->create($definition, "short")->validate(); - $this->assertEqual($violations->count(), 1); - $message = t('This value is too short. It should have %limit characters or more.', array('%limit' => 10)); - $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); - - // Test having multiple violations. - $definition = DataDefinition::create('integer') - ->setConstraints(array( - 'Range' => array('min' => 5), - 'Null' => array(), - )); - $violations = $this->typedDataManager->create($definition, 10)->validate(); - $this->assertEqual($violations->count(), 1); - $violations = $this->typedDataManager->create($definition, 1)->validate(); - $this->assertEqual($violations->count(), 2); - - // Test validating property containers and make sure the NotNull and Null - // constraints work with typed data containers. - $definition = BaseFieldDefinition::create('integer') - ->setConstraints(array('NotNull' => array())); - $field_item = $this->typedDataManager->create($definition, array('value' => 10)); - $violations = $field_item->validate(); - $this->assertEqual($violations->count(), 0); - - $field_item = $this->typedDataManager->create($definition, array('value' => 'no integer')); - $violations = $field_item->validate(); - $this->assertEqual($violations->count(), 1); - $this->assertEqual($violations[0]->getPropertyPath(), '0.value'); - - // Test that the field item may not be empty. - $field_item = $this->typedDataManager->create($definition); - $violations = $field_item->validate(); - $this->assertEqual($violations->count(), 1); - - // Test the Null constraint with typed data containers. - $definition = BaseFieldDefinition::create('float') - ->setConstraints(array('Null' => array())); - $field_item = $this->typedDataManager->create($definition, array('value' => 11.5)); - $violations = $field_item->validate(); - $this->assertEqual($violations->count(), 1); - $field_item = $this->typedDataManager->create($definition); - $violations = $field_item->validate(); - $this->assertEqual($violations->count(), 0); - - // Test getting constraint definitions by type. - $definitions = $this->typedDataManager->getValidationConstraintManager()->getDefinitionsByType('entity'); - $this->assertTrue(isset($definitions['EntityType']), 'Constraint plugin found for type entity.'); - $this->assertTrue(isset($definitions['Null']), 'Constraint plugin found for type entity.'); - $this->assertTrue(isset($definitions['NotNull']), 'Constraint plugin found for type entity.'); - - $definitions = $this->typedDataManager->getValidationConstraintManager()->getDefinitionsByType('string'); - $this->assertFalse(isset($definitions['EntityType']), 'Constraint plugin not found for type string.'); - $this->assertTrue(isset($definitions['Null']), 'Constraint plugin found for type string.'); - $this->assertTrue(isset($definitions['NotNull']), 'Constraint plugin found for type string.'); - - // Test automatic 'required' validation. - $definition = DataDefinition::create('integer') - ->setRequired(TRUE); - $violations = $this->typedDataManager->create($definition)->validate(); - $this->assertEqual($violations->count(), 1); - $violations = $this->typedDataManager->create($definition, 0)->validate(); - $this->assertEqual($violations->count(), 0); - - // Test validating a list of a values and make sure property paths starting - // with "0" are created. - $definition = BaseFieldDefinition::create('integer'); - $violations = $this->typedDataManager->create($definition, array(array('value' => 10)))->validate(); - $this->assertEqual($violations->count(), 0); - $violations = $this->typedDataManager->create($definition, array(array('value' => 'string')))->validate(); - $this->assertEqual($violations->count(), 1); - - $this->assertEqual($violations[0]->getInvalidValue(), 'string'); - $this->assertIdentical($violations[0]->getPropertyPath(), '0.value'); - } - -} diff --git a/core/modules/system/src/Tests/Update/CompatibilityFixTest.php b/core/modules/system/src/Tests/Update/CompatibilityFixTest.php deleted file mode 100644 index 6689c2f..0000000 --- a/core/modules/system/src/Tests/Update/CompatibilityFixTest.php +++ /dev/null @@ -1,44 +0,0 @@ -getEditable('core.extension'); - - // Add an incompatible/non-existent module to the config. - $modules = $extension_config->get('module'); - $modules['incompatible_module'] = 0; - $extension_config->set('module', $modules); - $modules = $extension_config->get('module'); - $this->assertTrue(in_array('incompatible_module', array_keys($modules)), 'Added incompatible/non-existent module to the config.'); - - // Add an incompatible/non-existent theme to the config. - $themes = $extension_config->get('theme'); - $themes['incompatible_theme'] = 0; - $extension_config->set('theme', $themes); - $themes = $extension_config->get('theme'); - $this->assertTrue(in_array('incompatible_theme', array_keys($themes)), 'Added incompatible/non-existent theme to the config.'); - - // Fix compatibility. - update_fix_compatibility(); - $modules = $extension_config->get('module'); - $this->assertFalse(in_array('incompatible_module', array_keys($modules)), 'Fixed modules compatibility.'); - $themes = $extension_config->get('theme'); - $this->assertFalse(in_array('incompatible_theme', array_keys($themes)), 'Fixed themes compatibility.'); - } - -} diff --git a/core/modules/system/src/Tests/Update/DbDumpTest.php b/core/modules/system/src/Tests/Update/DbDumpTest.php deleted file mode 100644 index 3a6a927..0000000 --- a/core/modules/system/src/Tests/Update/DbDumpTest.php +++ /dev/null @@ -1,277 +0,0 @@ -register('cache_factory', 'Drupal\Core\Cache\DatabaseBackendFactory') - ->addArgument(new Reference('database')) - ->addArgument(new Reference('cache_tags.invalidator.checksum')); - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Determine what database backend is running, and set the skip flag. - $this->skipTests = Database::getConnection()->databaseType() !== 'mysql'; - - // Create some schemas so our export contains tables. - $this->installSchema('system', [ - 'key_value_expire', - 'sessions', - ]); - $this->installSchema('dblog', ['watchdog']); - $this->installEntitySchema('block_content'); - $this->installEntitySchema('user'); - $this->installEntitySchema('file'); - $this->installEntitySchema('menu_link_content'); - $this->installSchema('system', 'sequences'); - - // Place some sample config to test for in the export. - $this->data = [ - 'foo' => $this->randomMachineName(), - 'bar' => $this->randomMachineName(), - ]; - $storage = new DatabaseStorage(Database::getConnection(), 'config'); - $storage->write('test_config', $this->data); - - // Create user account with some potential syntax issues. - $account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']); - $account->save(); - - // Create url_alias (this will create 'url_alias'). - $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example'); - - // Create a cache table (this will create 'cache_discovery'). - \Drupal::cache('discovery')->set('test', $this->data); - - // These are all the tables that should now be in place. - $this->tables = [ - 'block_content', - 'block_content_field_data', - 'block_content_field_revision', - 'block_content_revision', - 'cachetags', - 'config', - 'cache_bootstrap', - 'cache_data', - 'cache_default', - 'cache_discovery', - 'cache_entity', - 'file_managed', - 'key_value_expire', - 'menu_link_content', - 'menu_link_content_data', - 'sequences', - 'sessions', - 'url_alias', - 'user__roles', - 'users', - 'users_field_data', - 'watchdog', - ]; - } - - /** - * Test the command directly. - */ - public function testDbDumpCommand() { - if ($this->skipTests) { - $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql"); - return; - } - - $application = new DbDumpApplication(); - $command = $application->find('dump-database-d8-mysql'); - $command_tester = new CommandTester($command); - $command_tester->execute([]); - - // Tables that are schema-only should not have data exported. - $pattern = preg_quote("\$connection->insert('sessions')"); - $this->assertFalse(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Tables defined as schema-only do not have data exported to the script.'); - - // Table data is exported. - $pattern = preg_quote("\$connection->insert('config')"); - $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Table data is properly exported to the script.'); - - // The test data are in the dump (serialized). - $pattern = preg_quote(serialize($this->data)); - $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Generated data is found in the exported script.'); - - // Check that the user account name and email address was properly escaped. - $pattern = preg_quote('"q\'uote\$dollar@example.com"'); - $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account email address was properly escaped in the exported script.'); - $pattern = preg_quote('\'$dollar\''); - $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account name was properly escaped in the exported script.'); - } - - /** - * Test loading the script back into the database. - */ - public function testScriptLoad() { - if ($this->skipTests) { - $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql"); - return; - } - - // Generate the script. - $application = new DbDumpApplication(); - $command = $application->find('dump-database-d8-mysql'); - $command_tester = new CommandTester($command); - $command_tester->execute([]); - $script = $command_tester->getDisplay(); - - // Store original schemas and drop tables to avoid errors. - foreach ($this->tables as $table) { - $this->originalTableSchemas[$table] = $this->getTableSchema($table); - $this->originalTableIndexes[$table] = $this->getTableIndexes($table); - Database::getConnection()->schema()->dropTable($table); - } - - // This will load the data. - $file = sys_get_temp_dir() . '/' . $this->randomMachineName(); - file_put_contents($file, $script); - require_once $file; - - // The tables should now exist and the schemas should match the originals. - foreach ($this->tables as $table) { - $this->assertTrue(Database::getConnection() - ->schema() - ->tableExists($table), SafeMarkup::format('Table @table created by the database script.', ['@table' => $table])); - $this->assertIdentical($this->originalTableSchemas[$table], $this->getTableSchema($table), SafeMarkup::format('The schema for @table was properly restored.', ['@table' => $table])); - $this->assertIdentical($this->originalTableIndexes[$table], $this->getTableIndexes($table), SafeMarkup::format('The indexes for @table were properly restored.', ['@table' => $table])); - } - - // Ensure the test config has been replaced. - $config = unserialize(db_query("SELECT data FROM {config} WHERE name = 'test_config'")->fetchField()); - $this->assertIdentical($config, $this->data, 'Script has properly restored the config table data.'); - - // Ensure the cache data was not exported. - $this->assertFalse(\Drupal::cache('discovery') - ->get('test'), 'Cache data was not exported to the script.'); - } - - /** - * Helper function to get a simplified schema for a given table. - * - * @param string $table - * - * @return array - * Array keyed by field name, with the values being the field type. - */ - protected function getTableSchema($table) { - // Verify the field type on the data column in the cache table. - // @todo this is MySQL specific. - $query = db_query("SHOW COLUMNS FROM {" . $table . "}"); - $definition = []; - while ($row = $query->fetchAssoc()) { - $definition[$row['Field']] = $row['Type']; - } - return $definition; - } - - /** - * Returns indexes for a given table. - * - * @param string $table - * The table to find indexes for. - * - * @return array - * The 'primary key', 'unique keys', and 'indexes' portion of the Drupal - * table schema. - */ - protected function getTableIndexes($table) { - $query = db_query("SHOW INDEX FROM {" . $table . "}"); - $definition = []; - while ($row = $query->fetchAssoc()) { - $index_name = $row['Key_name']; - $column = $row['Column_name']; - // Key the arrays by the index sequence for proper ordering (start at 0). - $order = $row['Seq_in_index'] - 1; - - // If specified, add length to the index. - if ($row['Sub_part']) { - $column = [$column, $row['Sub_part']]; - } - - if ($index_name === 'PRIMARY') { - $definition['primary key'][$order] = $column; - } - elseif ($row['Non_unique'] == 0) { - $definition['unique keys'][$index_name][$order] = $column; - } - else { - $definition['indexes'][$index_name][$order] = $column; - } - } - return $definition; - } - -} diff --git a/core/modules/system/src/Tests/Utility/LinkGenerationTest.php b/core/modules/system/src/Tests/Utility/LinkGenerationTest.php deleted file mode 100644 index fabb6c7..0000000 --- a/core/modules/system/src/Tests/Utility/LinkGenerationTest.php +++ /dev/null @@ -1,65 +0,0 @@ -executeInRenderContext(new RenderContext(), function () use ($url) { - return \Drupal::l(['#markup' => 'link with markup'], $url); - }); - $this->setRawContent($link); - $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); - // Ensure the content of the link is not escaped. - $this->assertRaw('link with markup'); - - // Test just adding text to an already safe string. - \Drupal::state()->set('link_generation_test_link_alter', TRUE); - $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) { - return \Drupal::l(['#markup' => 'link with markup'], $url); - }); - $this->setRawContent($link); - $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); - // Ensure the content of the link is escaped. - $this->assertEscaped('link with markup Test!'); - - // Test passing a safe string to t(). - \Drupal::state()->set('link_generation_test_link_alter_safe', TRUE); - $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) { - return \Drupal::l(['#markup' => 'link with markup'], $url); - }); - $this->setRawContent($link); - $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); - // Ensure the content of the link is escaped. - $this->assertRaw('link with markup Test!'); - - // Test passing an unsafe string to t(). - $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) { - return \Drupal::l('link with markup', $url); - }); - $this->setRawContent($link); - $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); - // Ensure the content of the link is escaped. - $this->assertEscaped('link with markup'); - $this->assertRaw('Test!'); - } - -} diff --git a/core/modules/system/src/Tests/Validation/AllowedValuesConstraintValidatorTest.php b/core/modules/system/src/Tests/Validation/AllowedValuesConstraintValidatorTest.php deleted file mode 100644 index 3970166..0000000 --- a/core/modules/system/src/Tests/Validation/AllowedValuesConstraintValidatorTest.php +++ /dev/null @@ -1,54 +0,0 @@ -typedData = $this->container->get('typed_data_manager'); - } - - /** - * Tests the AllowedValues validation constraint validator. - * - * For testing we define an integer with a set of allowed values. - */ - public function testValidation() { - // Create a definition that specifies some AllowedValues. - $definition = DataDefinition::create('integer') - ->addConstraint('AllowedValues', array(1, 2, 3)); - - // Test the validation. - $typed_data = $this->typedData->create($definition, 1); - $violations = $typed_data->validate(); - $this->assertEqual($violations->count(), 0, 'Validation passed for correct value.'); - - // Test the validation when an invalid value is passed. - $typed_data = $this->typedData->create($definition, 4); - $violations = $typed_data->validate(); - $this->assertEqual($violations->count(), 1, 'Validation failed for incorrect value.'); - - // Make sure the information provided by a violation is correct. - $violation = $violations[0]; - $this->assertEqual($violation->getMessage(), t('The value you selected is not a valid choice.'), 'The message for invalid value is correct.'); - $this->assertEqual($violation->getRoot(), $typed_data, 'Violation root is correct.'); - $this->assertEqual($violation->getInvalidValue(), 4, 'The invalid value is set correctly in the violation.'); - } - -} diff --git a/core/modules/system/src/Tests/Validation/ComplexDataConstraintValidatorTest.php b/core/modules/system/src/Tests/Validation/ComplexDataConstraintValidatorTest.php deleted file mode 100644 index d75d405..0000000 --- a/core/modules/system/src/Tests/Validation/ComplexDataConstraintValidatorTest.php +++ /dev/null @@ -1,79 +0,0 @@ -typedData = $this->container->get('typed_data_manager'); - } - - /** - * Tests the ComplexData validation constraint validator. - * - * For testing a map including a constraint on one of its keys is defined. - */ - public function testValidation() { - // Create a definition that specifies some ComplexData constraint. - $definition = MapDataDefinition::create() - ->setPropertyDefinition('key', DataDefinition::create('integer')) - ->addConstraint('ComplexData', array( - 'key' => array( - 'AllowedValues' => array(1, 2, 3) - ), - )); - - // Test the validation. - $typed_data = $this->typedData->create($definition, array('key' => 1)); - $violations = $typed_data->validate(); - $this->assertEqual($violations->count(), 0, 'Validation passed for correct value.'); - - // Test the validation when an invalid value is passed. - $typed_data = $this->typedData->create($definition, array('key' => 4)); - $violations = $typed_data->validate(); - $this->assertEqual($violations->count(), 1, 'Validation failed for incorrect value.'); - - // Make sure the information provided by a violation is correct. - $violation = $violations[0]; - $this->assertEqual($violation->getMessage(), t('The value you selected is not a valid choice.'), 'The message for invalid value is correct.'); - $this->assertEqual($violation->getRoot(), $typed_data, 'Violation root is correct.'); - $this->assertEqual($violation->getInvalidValue(), 4, 'The invalid value is set correctly in the violation.'); - - // Test using the constraint with a map without the specified key. This - // should be ignored as long as there is no NotNull or NotBlank constraint. - $typed_data = $this->typedData->create($definition, array('foo' => 'bar')); - $violations = $typed_data->validate(); - $this->assertEqual($violations->count(), 0, 'Constraint on non-existing key is ignored.'); - - $definition = MapDataDefinition::create() - ->setPropertyDefinition('key', DataDefinition::create('integer')) - ->addConstraint('ComplexData', array( - 'key' => array( - 'NotNull' => array() - ), - )); - - $typed_data = $this->typedData->create($definition, array('foo' => 'bar')); - $violations = $typed_data->validate(); - $this->assertEqual($violations->count(), 1, 'Key is required.'); - } - -} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index cdaeba6..b8f5f5f 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -549,15 +549,22 @@ function system_requirements($phase) { // defined, the installer will create a valid config directory later, but // during runtime we must always display an error. if (!empty($GLOBALS['config_directories'])) { - foreach ($GLOBALS['config_directories'] as $type => $directory) { - $directories[] = config_get_config_directory($type); + foreach (array_keys(array_filter($GLOBALS['config_directories'])) as $type) { + $directory = config_get_config_directory($type); + if (!is_dir($directory)) { + $requirements['config directory ' . $type] = array( + 'title' => t('Configuration directory: %type', ['%type' => $type]), + 'description' => t('The directory %directory does not exist.', array('%directory' => $directory)), + 'severity' => REQUIREMENT_ERROR, + ); + } } } - elseif ($phase != 'install') { + if ($phase != 'install' && (empty($GLOBALS['config_directories']) || empty($GLOBALS['config_directories'][CONFIG_SYNC_DIRECTORY]) )) { $requirements['config directories'] = array( 'title' => t('Configuration directories'), 'value' => t('Not present'), - 'description' => t('Your %file file must define the $config_directories variable as an array containing the name of a directories in which configuration files can be written.', array('%file' => $site_path . '/settings.php')), + 'description' => t('Your %file file must define the $config_directories variable as an array containing the names of directories in which configuration files can be found. It must contain a %sync_key key.', array('%file' => $site_path . '/settings.php', '%sync_key' => CONFIG_SYNC_DIRECTORY)), 'severity' => REQUIREMENT_ERROR, ); } diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 5f75f2f..547b083 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -397,6 +397,13 @@ system.theme_settings_theme: requirements: _access: 'TRUE' +'': + path: '' + options: + _no_path: TRUE + requirements: + _access: 'TRUE' + '': path: '' diff --git a/core/modules/system/tests/modules/condition_test/src/Tests/ConditionTestDualUserTest.php b/core/modules/system/tests/modules/condition_test/src/Tests/ConditionTestDualUserTest.php deleted file mode 100644 index c5724e0..0000000 --- a/core/modules/system/tests/modules/condition_test/src/Tests/ConditionTestDualUserTest.php +++ /dev/null @@ -1,93 +0,0 @@ -installSchema('system', 'sequences'); - $this->installEntitySchema('user'); - - $this->anonymous = User::create(['uid' => 0]); - $this->authenticated = User::create(['uid' => 1]); - } - - /** - * Tests the dual user condition. - */ - public function testConditions() { - $this->doTestIdenticalUser(); - $this->doTestDifferentUser(); - } - - /** - * Tests with both contexts mapped to the same user. - */ - protected function doTestIdenticalUser() { - /** @var \Drupal\Core\Condition\ConditionPluginBase $condition */ - $condition = \Drupal::service('plugin.manager.condition') - ->createInstance('condition_test_dual_user') - // Map the anonymous user to both contexts. - ->setContextMapping([ - 'user1' => 'anonymous', - 'user2' => 'anonymous', - ]); - $definition = new ContextDefinition('entity:user'); - $contexts['anonymous'] = new Context($definition, $this->anonymous); - \Drupal::service('context.handler')->applyContextMapping($condition, $contexts); - $this->assertTrue($condition->execute()); - } - - /** - * Tests with each context mapped to different users. - */ - protected function doTestDifferentUser() { - /** @var \Drupal\Core\Condition\ConditionPluginBase $condition */ - $condition = \Drupal::service('plugin.manager.condition') - ->createInstance('condition_test_dual_user') - ->setContextMapping([ - 'user1' => 'anonymous', - 'user2' => 'authenticated', - ]); - $definition = new ContextDefinition('entity:user'); - $contexts['anonymous'] = new Context($definition, $this->anonymous); - $contexts['authenticated'] = new Context($definition, $this->authenticated); - \Drupal::service('context.handler')->applyContextMapping($condition, $contexts); - $this->assertFalse($condition->execute()); - } - -} diff --git a/core/modules/system/tests/modules/condition_test/src/Tests/OptionalContextConditionTest.php b/core/modules/system/tests/modules/condition_test/src/Tests/OptionalContextConditionTest.php deleted file mode 100644 index 8b15f86..0000000 --- a/core/modules/system/tests/modules/condition_test/src/Tests/OptionalContextConditionTest.php +++ /dev/null @@ -1,71 +0,0 @@ -createInstance('condition_test_optional_context') - ->setContextMapping([ - 'node' => 'node', - ]); - \Drupal::service('context.handler')->applyContextMapping($condition, []); - $this->assertTrue($condition->execute()); - } - - /** - * Tests with both contexts mapped to the same user. - */ - protected function testContextNoValue() { - /** @var \Drupal\Core\Condition\ConditionPluginBase $condition */ - $condition = \Drupal::service('plugin.manager.condition') - ->createInstance('condition_test_optional_context') - ->setContextMapping([ - 'node' => 'node', - ]); - $definition = new ContextDefinition('entity:node'); - $contexts['node'] = (new Context($definition)); - \Drupal::service('context.handler')->applyContextMapping($condition, $contexts); - $this->assertTrue($condition->execute()); - } - - /** - * Tests with both contexts mapped to the same user. - */ - protected function testContextAvailable() { - NodeType::create(['type' => 'example', 'name' => 'Example'])->save(); - /** @var \Drupal\Core\Condition\ConditionPluginBase $condition */ - $condition = \Drupal::service('plugin.manager.condition') - ->createInstance('condition_test_optional_context') - ->setContextMapping([ - 'node' => 'node', - ]); - $definition = new ContextDefinition('entity:node'); - $node = Node::create(['type' => 'example']); - $contexts['node'] = new Context($definition, $node); - \Drupal::service('context.handler')->applyContextMapping($condition, $contexts); - $this->assertFalse($condition->execute()); - } - -} diff --git a/core/modules/system/tests/modules/early_rendering_controller_test/src/CacheableTestDomainObject.php b/core/modules/system/tests/modules/early_rendering_controller_test/src/CacheableTestDomainObject.php index 843d785..9da6fd8 100644 --- a/core/modules/system/tests/modules/early_rendering_controller_test/src/CacheableTestDomainObject.php +++ b/core/modules/system/tests/modules/early_rendering_controller_test/src/CacheableTestDomainObject.php @@ -3,28 +3,10 @@ namespace Drupal\early_rendering_controller_test; use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\UncacheableDependencyTrait; class CacheableTestDomainObject extends TestDomainObject implements CacheableDependencyInterface { - /** - * {@inheritdoc} - */ - public function getCacheContexts() { - return []; - } - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return []; - } - - /** - * {@inheritdoc} - */ - public function getCacheMaxAge() { - return 0; - } + use UncacheableDependencyTrait; } diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module index 9d9d63c..beb68ba 100644 --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -320,7 +320,11 @@ function entity_test_form_node_form_alter(&$form, FormStateInterface $form_state * The loaded entity object, or NULL if the entity cannot be loaded. */ function entity_test_load($id, $reset = FALSE) { - return entity_load('entity_test', $id, $reset); + $storage = \Drupal::entityTypeManager()->getStorage('entity_test'); + if ($reset) { + $storage->resetCache([$id]); + } + return $storage->load($id); } /** @@ -335,7 +339,11 @@ function entity_test_load($id, $reset = FALSE) { * The loaded entity object, or NULL if the entity cannot be loaded. */ function entity_test_rev_load($id, $reset = FALSE) { - return entity_load('entity_test_rev', $id, $reset); + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_rev'); + if ($reset) { + $storage->resetCache([$id]); + } + return $storage->load($id); } /** @@ -350,7 +358,11 @@ function entity_test_rev_load($id, $reset = FALSE) { * The loaded entity object, or FALSE if the entity cannot be loaded. */ function entity_test_mul_load($id, $reset = FALSE) { - return entity_load('entity_test_mul', $id, $reset); + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mul'); + if ($reset) { + $storage->resetCache([$id]); + } + return $storage->load($id); } /** @@ -365,7 +377,11 @@ function entity_test_mul_load($id, $reset = FALSE) { * The loaded entity object, or NULL if the entity cannot be loaded. */ function entity_test_mulrev_load($id, $reset = FALSE) { - return entity_load('entity_test_mulrev', $id, $reset); + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev'); + if ($reset) { + $storage->resetCache([$id]); + } + return $storage->load($id); } /** diff --git a/core/modules/system/tests/src/Kernel/Action/ActionTest.php b/core/modules/system/tests/src/Kernel/Action/ActionTest.php new file mode 100644 index 0000000..4ca24fb --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Action/ActionTest.php @@ -0,0 +1,101 @@ +actionManager = $this->container->get('plugin.manager.action'); + $this->installEntitySchema('user'); + $this->installSchema('system', array('sequences')); + } + + /** + * Tests the functionality of test actions. + */ + public function testOperations() { + // Test that actions can be discovered. + $definitions = $this->actionManager->getDefinitions(); + $this->assertTrue(count($definitions) > 1, 'Action definitions are found.'); + $this->assertTrue(!empty($definitions['action_test_no_type']), 'The test action is among the definitions found.'); + + $definition = $this->actionManager->getDefinition('action_test_no_type'); + $this->assertTrue(!empty($definition), 'The test action definition is found.'); + + $definitions = $this->actionManager->getDefinitionsByType('user'); + $this->assertTrue(empty($definitions['action_test_no_type']), 'An action with no type is not found.'); + + // Create an instance of the 'save entity' action. + $action = $this->actionManager->createInstance('action_test_save_entity'); + $this->assertTrue($action instanceof ActionInterface, 'The action implements the correct interface.'); + + // Create a new unsaved user. + $name = $this->randomMachineName(); + $user_storage = $this->container->get('entity.manager')->getStorage('user'); + $account = $user_storage->create(array('name' => $name, 'bundle' => 'user')); + $loaded_accounts = $user_storage->loadMultiple(); + $this->assertEqual(count($loaded_accounts), 0); + + // Execute the 'save entity' action. + $action->execute($account); + $loaded_accounts = $user_storage->loadMultiple(); + $this->assertEqual(count($loaded_accounts), 1); + $account = reset($loaded_accounts); + $this->assertEqual($name, $account->label()); + } + + /** + * Tests the dependency calculation of actions. + */ + public function testDependencies() { + // Create a new action that depends on a user role. + $action = Action::create(array( + 'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID, + 'type' => 'user', + 'label' => t('Add the anonymous role to the selected users'), + 'configuration' => array( + 'rid' => RoleInterface::ANONYMOUS_ID, + ), + 'plugin' => 'user_add_role_action', + )); + $action->save(); + + $expected = array( + 'config' => array( + 'user.role.' . RoleInterface::ANONYMOUS_ID, + ), + 'module' => array( + 'user', + ), + ); + $this->assertIdentical($expected, $action->calculateDependencies()->getDependencies()); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Block/SystemMenuBlockTest.php b/core/modules/system/tests/src/Kernel/Block/SystemMenuBlockTest.php new file mode 100644 index 0000000..ca1c5af --- /dev/null +++ b/core/modules/system/tests/src/Kernel/Block/SystemMenuBlockTest.php @@ -0,0 +1,311 @@ +installSchema('system', 'sequences'); + $this->installEntitySchema('user'); + $this->installEntitySchema('menu_link_content'); + + $account = User::create([ + 'name' => $this->randomMachineName(), + 'status' => 1, + ]); + $account->save(); + $this->container->get('current_user')->setAccount($account); + + $this->menuLinkManager = $this->container->get('plugin.manager.menu.link'); + $this->linkTree = $this->container->get('menu.link_tree'); + $this->blockManager = $this->container->get('plugin.manager.block'); + + $routes = new RouteCollection(); + $requirements = array('_access' => 'TRUE'); + $options = array('_access_checks' => array('access_check.default')); + $routes->add('example1', new Route('/example1', array(), $requirements, $options)); + $routes->add('example2', new Route('/example2', array(), $requirements, $options)); + $routes->add('example3', new Route('/example3', array(), $requirements, $options)); + $routes->add('example4', new Route('/example4', array(), $requirements, $options)); + $routes->add('example5', new Route('/example5', array(), $requirements, $options)); + $routes->add('example6', new Route('/example6', array(), $requirements, $options)); + $routes->add('example7', new Route('/example7', array(), $requirements, $options)); + $routes->add('example8', new Route('/example8', array(), $requirements, $options)); + + $mock_route_provider = new MockRouteProvider($routes); + $this->container->set('router.route_provider', $mock_route_provider); + + // Add a new custom menu. + $menu_name = 'mock'; + $label = $this->randomMachineName(16); + + $this->menu = Menu::create(array( + 'id' => $menu_name, + 'label' => $label, + 'description' => 'Description text', + )); + $this->menu->save(); + + // This creates a tree with the following structure: + // - 1 + // - 2 + // - 3 + // - 4 + // - 5 + // - 7 + // - 6 + // - 8 + // With link 6 being the only external link. + $links = array( + 1 => MenuLinkMock::create(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '', 'weight' => 0)), + 2 => MenuLinkMock::create(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => '', 'route_parameters' => array('foo' => 'bar'), 'weight' => 1)), + 3 => MenuLinkMock::create(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'weight' => 2)), + 4 => MenuLinkMock::create(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3', 'weight' => 3)), + 5 => MenuLinkMock::create(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '', 'expanded' => TRUE, 'weight' => 4)), + 6 => MenuLinkMock::create(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://www.drupal.org/', 'title' => 'barbar', 'parent' => '', 'weight' => 5)), + 7 => MenuLinkMock::create(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => 'test.example5', 'weight' => 6)), + 8 => MenuLinkMock::create(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '', 'weight' => 7)), + ); + foreach ($links as $instance) { + $this->menuLinkManager->addDefinition($instance->getPluginId(), $instance->getPluginDefinition()); + } + } + + /** + * Tests calculation of a system menu block's configuration dependencies. + */ + public function testSystemMenuBlockConfigDependencies() { + + $block = Block::create(array( + 'plugin' => 'system_menu_block:' . $this->menu->id(), + 'region' => 'footer', + 'id' => 'machinename', + 'theme' => 'stark', + )); + + $dependencies = $block->calculateDependencies()->getDependencies(); + $expected = array( + 'config' => array( + 'system.menu.' . $this->menu->id() + ), + 'module' => array( + 'system' + ), + 'theme' => array( + 'stark' + ), + ); + $this->assertIdentical($expected, $dependencies); + } + + /** + * Tests the config start level and depth. + */ + public function testConfigLevelDepth() { + // Helper function to generate a configured block instance. + $place_block = function ($level, $depth) { + return $this->blockManager->createInstance('system_menu_block:' . $this->menu->id(), array( + 'region' => 'footer', + 'id' => 'machinename', + 'theme' => 'stark', + 'level' => $level, + 'depth' => $depth, + )); + }; + + // All the different block instances we're going to test. + $blocks = [ + 'all' => $place_block(1, 0), + 'level_1_only' => $place_block(1, 1), + 'level_2_only' => $place_block(2, 1), + 'level_3_only' => $place_block(3, 1), + 'level_1_and_beyond' => $place_block(1, 0), + 'level_2_and_beyond' => $place_block(2, 0), + 'level_3_and_beyond' => $place_block(3, 0), + ]; + + // Scenario 1: test all block instances when there's no active trail. + $no_active_trail_expectations = []; + $no_active_trail_expectations['all'] = [ + 'test.example1' => [], + 'test.example2' => [], + 'test.example5' => [ + 'test.example7' => [], + ], + 'test.example6' => [], + 'test.example8' => [], + ]; + $no_active_trail_expectations['level_1_only'] = [ + 'test.example1' => [], + 'test.example2' => [], + 'test.example5' => [], + 'test.example6' => [], + 'test.example8' => [], + ]; + $no_active_trail_expectations['level_2_only'] = [ + 'test.example7' => [], + ]; + $no_active_trail_expectations['level_3_only'] = []; + $no_active_trail_expectations['level_1_and_beyond'] = $no_active_trail_expectations['all']; + $no_active_trail_expectations['level_2_and_beyond'] = $no_active_trail_expectations['level_2_only']; + $no_active_trail_expectations['level_3_and_beyond'] = []; + foreach ($blocks as $id => $block) { + $block_build = $block->build(); + $items = isset($block_build['#items']) ? $block_build['#items'] : []; + $this->assertIdentical($no_active_trail_expectations[$id], $this->convertBuiltMenuToIdTree($items), format_string('Menu block %id with no active trail renders the expected tree.', ['%id' => $id])); + } + + // Scenario 2: test all block instances when there's an active trail. + $route = $this->container->get('router.route_provider')->getRouteByName('example3'); + $request = new Request(); + $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'example3'); + $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $route); + $this->container->get('request_stack')->push($request); + // \Drupal\Core\Menu\MenuActiveTrail uses the cache collector pattern, which + // includes static caching. Since this second scenario simulates a second + // request, we must also simulate it for the MenuActiveTrail service, by + // clearing the cache collector's static cache. + \Drupal::service('menu.active_trail')->clear(); + + $active_trail_expectations = []; + $active_trail_expectations['all'] = [ + 'test.example1' => [], + 'test.example2' => [ + 'test.example3' => [ + 'test.example4' => [], + ] + ], + 'test.example5' => [ + 'test.example7' => [], + ], + 'test.example6' => [], + 'test.example8' => [], + ]; + $active_trail_expectations['level_1_only'] = [ + 'test.example1' => [], + 'test.example2' => [], + 'test.example5' => [], + 'test.example6' => [], + 'test.example8' => [], + ]; + $active_trail_expectations['level_2_only'] = [ + 'test.example3' => [], + 'test.example7' => [], + ]; + $active_trail_expectations['level_3_only'] = [ + 'test.example4' => [], + ]; + $active_trail_expectations['level_1_and_beyond'] = $active_trail_expectations['all']; + $active_trail_expectations['level_2_and_beyond'] = [ + 'test.example3' => [ + 'test.example4' => [], + ], + 'test.example7' => [], + ]; + $active_trail_expectations['level_3_and_beyond'] = $active_trail_expectations['level_3_only']; + foreach ($blocks as $id => $block) { + $block_build = $block->build(); + $items = isset($block_build['#items']) ? $block_build['#items'] : []; + $this->assertIdentical($active_trail_expectations[$id], $this->convertBuiltMenuToIdTree($items), format_string('Menu block %id with an active trail renders the expected tree.', ['%id' => $id])); + } + } + + /** + * Helper method to allow for easy menu link tree structure assertions. + * + * Converts the result of MenuLinkTree::build() in a "menu link ID tree". + * + * @param array $build + * The return value of MenuLinkTree::build() + * + * @return array + * The "menu link ID tree" representation of the given render array. + */ + protected function convertBuiltMenuToIdTree(array $build) { + $level = []; + foreach (Element::children($build) as $id) { + $level[$id] = []; + if (isset($build[$id]['below'])) { + $level[$id] = $this->convertBuiltMenuToIdTree($build[$id]['below']); + } + } + return $level; + } + +} diff --git a/core/modules/system/tests/src/Kernel/System/InfoAlterTest.php b/core/modules/system/tests/src/Kernel/System/InfoAlterTest.php new file mode 100644 index 0000000..8e6f79e --- /dev/null +++ b/core/modules/system/tests/src/Kernel/System/InfoAlterTest.php @@ -0,0 +1,36 @@ +set('module_required_test.hook_system_info_alter', TRUE); + $info = system_rebuild_module_data(); + $this->assertFalse(isset($info['node']->info['required']), 'Before the module_required_test is installed the node module is not required.'); + + // Enable the test module. + \Drupal::service('module_installer')->install(array('module_required_test'), FALSE); + $this->assertTrue(\Drupal::moduleHandler()->moduleExists('module_required_test'), 'Test required module is enabled.'); + + $info = system_rebuild_module_data(); + $this->assertTrue($info['node']->info['required'], 'After the module_required_test is installed the node module is required.'); + } + +} diff --git a/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml b/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml index 2b962d6..b42fbae 100644 --- a/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml +++ b/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml @@ -1,6 +1,6 @@ # Themes are not supposed to provide/install this kind of config normally. # This exists for testing purposes only. -# @see \Drupal\system\Tests\Extension\ThemeInstallerTest +# @see \Drupal\KernelTests\Core\Theme\ThemeInstallerTest id: fancy label: 'Fancy date' status: true diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 65386479..2e41e2d 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -101,33 +101,18 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { * {@inheritdoc} */ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields['tid'] = BaseFieldDefinition::create('integer') - ->setLabel(t('Term ID')) - ->setDescription(t('The term ID.')) - ->setReadOnly(TRUE) - ->setSetting('unsigned', TRUE); - - $fields['uuid'] = BaseFieldDefinition::create('uuid') - ->setLabel(t('UUID')) - ->setDescription(t('The term UUID.')) - ->setReadOnly(TRUE); - - $fields['vid'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Vocabulary')) - ->setDescription(t('The vocabulary to which the term is assigned.')) - ->setSetting('target_type', 'taxonomy_vocabulary'); - - $fields['langcode'] = BaseFieldDefinition::create('language') - ->setLabel(t('Language')) - ->setDescription(t('The term language code.')) - ->setTranslatable(TRUE) - ->setDisplayOptions('view', array( - 'type' => 'hidden', - )) - ->setDisplayOptions('form', array( - 'type' => 'language_select', - 'weight' => 2, - )); + /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */ + $fields = parent::baseFieldDefinitions($entity_type); + + $fields['tid']->setLabel(t('Term ID')) + ->setDescription(t('The term ID.')); + + $fields['uuid']->setDescription(t('The term UUID.')); + + $fields['vid']->setLabel(t('Vocabulary')) + ->setDescription(t('The vocabulary to which the term is assigned.')); + + $fields['langcode']->setDescription(t('The term language code.')); $fields['name'] = BaseFieldDefinition::create('string') ->setLabel(t('Name')) diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyIndexTidUiTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyIndexTidUiTest.php index b37a717..7dff50a 100644 --- a/core/modules/taxonomy/src/Tests/Views/TaxonomyIndexTidUiTest.php +++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyIndexTidUiTest.php @@ -7,6 +7,7 @@ use Drupal\taxonomy\Entity\Vocabulary; use Drupal\views\Tests\ViewTestData; use Drupal\views_ui\Tests\UITestBase; +use Drupal\views\Entity\View; /** * Tests the taxonomy index filter handler UI. @@ -102,7 +103,7 @@ public function testFilterUI() { // Ensure the autocomplete input element appears when using the 'textfield' // type. - $view = entity_load('view', 'test_filter_taxonomy_index_tid'); + $view = View::load('test_filter_taxonomy_index_tid'); $display =& $view->getDisplay('default'); $display['display_options']['filters']['tid']['type'] = 'textfield'; $view->save(); diff --git a/core/modules/telephone/tests/src/Kernel/TelephoneItemTest.php b/core/modules/telephone/tests/src/Kernel/TelephoneItemTest.php index 2187c36..7f94ba3 100644 --- a/core/modules/telephone/tests/src/Kernel/TelephoneItemTest.php +++ b/core/modules/telephone/tests/src/Kernel/TelephoneItemTest.php @@ -52,7 +52,7 @@ public function testTestItem() { // Verify entity has been created properly. $id = $entity->id(); - $entity = entity_load('entity_test', $id); + $entity = EntityTest::load($id); $this->assertTrue($entity->field_test instanceof FieldItemListInterface, 'Field implements interface.'); $this->assertTrue($entity->field_test[0] instanceof FieldItemInterface, 'Field item implements interface.'); $this->assertEqual($entity->field_test->value, $value); @@ -65,7 +65,7 @@ public function testTestItem() { // Read changed entity and assert changed values. $entity->save(); - $entity = entity_load('entity_test', $id); + $entity = EntityTest::load($id); $this->assertEqual($entity->field_test->value, $new_value); // Test sample item generation. diff --git a/core/modules/text/src/Tests/TextFieldTest.php b/core/modules/text/src/Tests/TextFieldTest.php index fcec361..993346a 100644 --- a/core/modules/text/src/Tests/TextFieldTest.php +++ b/core/modules/text/src/Tests/TextFieldTest.php @@ -7,6 +7,7 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Tests\String\StringFieldTest; use Drupal\field\Entity\FieldStorageConfig; +use Drupal\filter\Entity\FilterFormat; /** * Tests the creation of text fields. @@ -195,7 +196,7 @@ function _testTextfieldWidgetsFormatted($field_type, $widget_type) { $this->assertText(t('entity_test @id has been created.', array('@id' => $id)), 'Entity was created'); // Display the entity. - $entity = entity_load('entity_test', $id); + $entity = EntityTest::load($id); $display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full'); $content = $display->build($entity); $this->setRawContent($renderer->renderRoot($content)); @@ -211,7 +212,7 @@ function _testTextfieldWidgetsFormatted($field_type, $widget_type) { ); $this->drupalPostForm('admin/config/content/formats/add', $edit, t('Save configuration')); filter_formats_reset(); - $format = entity_load('filter_format', $edit['format']); + $format = FilterFormat::load($edit['format']); $format_id = $format->id(); $permission = $format->getPermissionName(); $roles = $this->webUser->getRoles(); @@ -234,7 +235,7 @@ function _testTextfieldWidgetsFormatted($field_type, $widget_type) { // Display the entity. $this->container->get('entity.manager')->getStorage('entity_test')->resetCache(array($id)); - $entity = entity_load('entity_test', $id); + $entity = EntityTest::load($id); $display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full'); $content = $display->build($entity); $this->setRawContent($renderer->renderRoot($content)); diff --git a/core/modules/text/text.module b/core/modules/text/text.module index a3dbcec..906b461 100644 --- a/core/modules/text/text.module +++ b/core/modules/text/text.module @@ -8,6 +8,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\filter\Entity\FilterFormat; /** * Implements hook_help(). @@ -80,7 +81,7 @@ function text_summary($text, $format = NULL, $size = NULL) { // Retrieve the filters of the specified text format, if any. if (isset($format)) { - $filters = entity_load('filter_format', $format)->filters(); + $filters = FilterFormat::load($format)->filters(); // If the specified format does not exist, return nothing. $text is already // filtered text, but the remainder of this function will not be able to // ensure a sane and secure summary. diff --git a/core/modules/tour/src/Tests/TourTest.php b/core/modules/tour/src/Tests/TourTest.php index ab2fe4a..78304c6 100644 --- a/core/modules/tour/src/Tests/TourTest.php +++ b/core/modules/tour/src/Tests/TourTest.php @@ -145,7 +145,7 @@ public function testTourFunctionality() { $this->drupalGet('tour-test-1'); // Load it back from the database and verify storage worked. - $entity_save_tip = entity_load('tour', 'tour-entity-create-test-en'); + $entity_save_tip = Tour::load('tour-entity-create-test-en'); // Verify that hook_ENTITY_TYPE_load() integration worked. $this->assertEqual($entity_save_tip->loaded, 'Load hooks work'); // Verify that hook_ENTITY_TYPE_presave() integration worked. diff --git a/core/modules/tour/tour.module b/core/modules/tour/tour.module index ed53f25..050eaa7 100644 --- a/core/modules/tour/tour.module +++ b/core/modules/tour/tour.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\tour\Entity\Tour; /** * Implements hook_help(). @@ -85,7 +86,7 @@ function tour_page_bottom(array &$page_bottom) { $results = \Drupal::entityQuery('tour') ->condition('routes.*.route_name', $route_name) ->execute(); - if (!empty($results) && $tours = entity_load_multiple('tour', array_keys($results))) { + if (!empty($results) && $tours = Tour::loadMultiple(array_keys($results))) { foreach ($tours as $id => $tour) { // Match on params. if (!$tour->hasMatchingRoute($route_name, $route_match->getRawParameters()->all())) { diff --git a/core/modules/tour/tour.permissions.yml b/core/modules/tour/tour.permissions.yml index fa14b8a..d5a4ebc 100644 --- a/core/modules/tour/tour.permissions.yml +++ b/core/modules/tour/tour.permissions.yml @@ -1,3 +1,2 @@ access tour: - title: 'Access tour' - description: 'View tour tips.' + title: 'Access tours' diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php index 4896bad..030b9a9 100644 --- a/core/modules/user/src/Entity/User.php +++ b/core/modules/user/src/Entity/User.php @@ -425,21 +425,17 @@ public static function getAnonymousUser() { * {@inheritdoc} */ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - $fields['uid'] = BaseFieldDefinition::create('integer') - ->setLabel(t('User ID')) - ->setDescription(t('The user ID.')) - ->setReadOnly(TRUE) - ->setSetting('unsigned', TRUE); - - $fields['uuid'] = BaseFieldDefinition::create('uuid') - ->setLabel(t('UUID')) - ->setDescription(t('The user UUID.')) - ->setReadOnly(TRUE); - - $fields['langcode'] = BaseFieldDefinition::create('language') - ->setLabel(t('Language code')) + /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */ + $fields = parent::baseFieldDefinitions($entity_type); + + $fields['uid']->setLabel(t('User ID')) + ->setDescription(t('The user ID.')); + + $fields['uuid']->setDescription(t('The user UUID.')); + + $fields['langcode']->setLabel(t('Language code')) ->setDescription(t('The user language code.')) - ->setTranslatable(TRUE); + ->setDisplayOptions('form', ['type' => 'hidden']); $fields['preferred_langcode'] = BaseFieldDefinition::create('language') ->setLabel(t('Preferred language code')) diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php index a8d8666..43b29e5 100644 --- a/core/modules/user/src/Form/UserLoginForm.php +++ b/core/modules/user/src/Form/UserLoginForm.php @@ -232,7 +232,7 @@ public function validateFinal(array &$form, FormStateInterface $form_state) { // handlers that ran earlier than this one. $user_input = $form_state->getUserInput(); $query = isset($user_input['name']) ? array('name' => $user_input['name']) : array(); - $form_state->setErrorByName('name', $this->t('Unrecognized username or password. Have you forgotten your password?', array(':password' => $this->url('user.pass', [], array('query' => $query))))); + $form_state->setErrorByName('name', $this->t('Unrecognized username or password. Forgot your password?', array(':password' => $this->url('user.pass', [], array('query' => $query))))); $accounts = $this->userStorage->loadByProperties(array('name' => $form_state->getValue('name'))); if (!empty($accounts)) { $this->logger('user')->notice('Login attempt failed for %user.', array('%user' => $form_state->getValue('name'))); diff --git a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php index ba6a18f..fa71464 100644 --- a/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php +++ b/core/modules/user/src/Plugin/Validation/Constraint/UserNameConstraintValidator.php @@ -29,7 +29,7 @@ public function validate($items, Constraint $constraint) { if (strpos($name, ' ') !== FALSE) { $this->context->addViolation($constraint->multipleSpacesMessage); } - if (preg_match('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', $name) + if (preg_match('/[^\x{80}-\x{F7} a-z0-9@+_.\'-]/i', $name) || preg_match( // Non-printable ISO-8859-1 + NBSP '/[\x{80}-\x{A0}' . diff --git a/core/modules/user/src/Plugin/views/filter/Name.php b/core/modules/user/src/Plugin/views/filter/Name.php index d31c781..5f63cb3 100644 --- a/core/modules/user/src/Plugin/views/filter/Name.php +++ b/core/modules/user/src/Plugin/views/filter/Name.php @@ -108,7 +108,8 @@ public function adminSummary() { $this->valueOptions = array(); if ($this->value) { - $result = entity_load_multiple_by_properties('user', array('uid' => $this->value)); + $result = \Drupal::entityTypeManager()->getStorage('user') + ->loadByProperties(['uid' => $this->value]); foreach ($result as $account) { if ($account->id()) { $this->valueOptions[$account->id()] = $account->label(); diff --git a/core/modules/user/src/Tests/UserCancelTest.php b/core/modules/user/src/Tests/UserCancelTest.php index 2dd8006..fe21516 100644 --- a/core/modules/user/src/Tests/UserCancelTest.php +++ b/core/modules/user/src/Tests/UserCancelTest.php @@ -353,7 +353,7 @@ function testUserAnonymize() { $test_node = $node_storage->load($node->id()); $this->assertTrue(($test_node->getOwnerId() == 0 && $test_node->isPublished()), 'Node of the user has been attributed to anonymous user.'); $test_node = node_revision_load($revision, TRUE); - $this->assertTrue(($test_node->getRevisionAuthor()->id() == 0 && $test_node->isPublished()), 'Node revision of the user has been attributed to anonymous user.'); + $this->assertTrue(($test_node->getRevisionUser()->id() == 0 && $test_node->isPublished()), 'Node revision of the user has been attributed to anonymous user.'); $node_storage->resetCache(array($revision_node->id())); $test_node = $node_storage->load($revision_node->id()); $this->assertTrue(($test_node->getOwnerId() != 0 && $test_node->isPublished()), "Current revision of the user's node was not attributed to anonymous user."); diff --git a/core/modules/user/src/Tests/UserLoginTest.php b/core/modules/user/src/Tests/UserLoginTest.php index 5d7d58e..af00c74 100644 --- a/core/modules/user/src/Tests/UserLoginTest.php +++ b/core/modules/user/src/Tests/UserLoginTest.php @@ -171,7 +171,7 @@ function assertFailedLogin($account, $flood_trigger = NULL) { } } else { - $this->assertText(t('Unrecognized username or password. Have you forgotten your password?')); + $this->assertText(t('Unrecognized username or password. Forgot your password?')); } } diff --git a/core/modules/user/src/Tests/UserPasswordResetTest.php b/core/modules/user/src/Tests/UserPasswordResetTest.php index af8f56c..0fa89e5 100644 --- a/core/modules/user/src/Tests/UserPasswordResetTest.php +++ b/core/modules/user/src/Tests/UserPasswordResetTest.php @@ -224,7 +224,7 @@ public function testUserResetPasswordTextboxFilled() { 'pass' => $this->randomMachineName(), ); $this->drupalPostForm('user/login', $edit, t('Log in')); - $this->assertRaw(t('Unrecognized username or password. Have you forgotten your password?', + $this->assertRaw(t('Unrecognized username or password. Forgot your password?', array(':password' => \Drupal::url('user.pass', [], array('query' => array('name' => $edit['name'])))))); unset($edit['pass']); $this->drupalGet('user/password', array('query' => array('name' => $edit['name']))); diff --git a/core/modules/user/src/Tests/UserRegistrationTest.php b/core/modules/user/src/Tests/UserRegistrationTest.php index 56f21e4..0831362 100644 --- a/core/modules/user/src/Tests/UserRegistrationTest.php +++ b/core/modules/user/src/Tests/UserRegistrationTest.php @@ -40,7 +40,10 @@ function testRegistrationWithEmailVerification() { $edit['mail'] = $mail = $edit['name'] . '@example.com'; $this->drupalPostForm('user/register', $edit, t('Create new account')); $this->assertText(t('A welcome message with further instructions has been sent to your email address.'), 'User registered successfully.'); - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + + /** @var EntityStorageInterface $storage */ + $storage = $this->container->get('entity_type.manager')->getStorage('user'); + $accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]); $new_user = reset($accounts); $this->assertTrue($new_user->isActive(), 'New account is active after registration.'); $resetURL = user_pass_reset_url($new_user); @@ -54,7 +57,7 @@ function testRegistrationWithEmailVerification() { $edit['mail'] = $mail = $edit['name'] . '@example.com'; $this->drupalPostForm('user/register', $edit, t('Create new account')); $this->container->get('entity.manager')->getStorage('user')->resetCache(); - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + $accounts = $storage->loadByProperties(['name' => $name, 'mail' => $mail]); $new_user = reset($accounts); $this->assertFalse($new_user->isActive(), 'New account is blocked until approved by an administrator.'); } @@ -83,7 +86,8 @@ function testRegistrationWithoutEmailVerification() { $edit['pass[pass2]'] = $new_pass; $this->drupalPostForm('user/register', $edit, t('Create new account')); $this->container->get('entity.manager')->getStorage('user')->resetCache(); - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + $accounts = $this->container->get('entity_type.manager')->getStorage('user') + ->loadByProperties(['name' => $name, 'mail' => $mail]); $new_user = reset($accounts); $this->assertNotNull($new_user, 'New account successfully created with matching passwords.'); $this->assertText(t('Registration successful. You are now logged in.'), 'Users are logged in after registering.'); @@ -108,7 +112,8 @@ function testRegistrationWithoutEmailVerification() { $this->assertText(t('The username @name has not been activated or is blocked.', array('@name' => $name)), 'User cannot log in yet.'); // Activate the new account. - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + $accounts = $this->container->get('entity_type.manager')->getStorage('user') + ->loadByProperties(['name' => $name, 'mail' => $mail]); $new_user = reset($accounts); $admin_user = $this->drupalCreateUser(array('administer users')); $this->drupalLogin($admin_user); @@ -248,7 +253,8 @@ function testRegistrationDefaultValues() { $this->drupalPostForm(NULL, $edit, t('Create new account')); // Check user fields. - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + $accounts = $this->container->get('entity_type.manager')->getStorage('user') + ->loadByProperties(['name' => $name, 'mail' => $mail]); $new_user = reset($accounts); $this->assertEqual($new_user->getUsername(), $name, 'Username matches.'); $this->assertEqual($new_user->getEmail(), $mail, 'Email address matches.'); @@ -338,7 +344,8 @@ function testRegistrationWithUserFields() { $edit['test_user_field[0][value]'] = $value; $this->drupalPostForm(NULL, $edit, t('Create new account')); // Check user fields. - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + $accounts = $this->container->get('entity_type.manager')->getStorage('user') + ->loadByProperties(['name' => $name, 'mail' => $mail]); $new_user = reset($accounts); $this->assertEqual($new_user->test_user_field->value, $value, 'The field value was correctly saved.'); @@ -367,7 +374,8 @@ function testRegistrationWithUserFields() { $edit['mail'] = $mail = $edit['name'] . '@example.com'; $this->drupalPostForm(NULL, $edit, t('Create new account')); // Check user fields. - $accounts = entity_load_multiple_by_properties('user', array('name' => $name, 'mail' => $mail)); + $accounts = $this->container->get('entity_type.manager')->getStorage('user') + ->loadByProperties(array('name' => $name, 'mail' => $mail)); $new_user = reset($accounts); $this->assertEqual($new_user->test_user_field[0]->value, $value, format_string('@js : The field value was correctly saved.', array('@js' => $js))); $this->assertEqual($new_user->test_user_field[1]->value, $value + 1, format_string('@js : The field value was correctly saved.', array('@js' => $js))); diff --git a/core/modules/user/src/Tests/UserSearchTest.php b/core/modules/user/src/Tests/UserSearchTest.php index 6000f39..d154030 100644 --- a/core/modules/user/src/Tests/UserSearchTest.php +++ b/core/modules/user/src/Tests/UserSearchTest.php @@ -21,8 +21,9 @@ class UserSearchTest extends WebTestBase { function testUserSearch() { // Verify that a user without 'administer users' permission cannot search - // for users by email address. - $user1 = $this->drupalCreateUser(array('access user profiles', 'search content')); + // for users by email address. Additionally, ensure that the username has a + // plus sign to ensure searching works with that. + $user1 = $this->drupalCreateUser(array('access user profiles', 'search content'), "foo+bar"); $this->drupalLogin($user1); $keys = $user1->getEmail(); $edit = array('keys' => $keys); diff --git a/core/modules/user/src/Tests/UserTimeZoneTest.php b/core/modules/user/src/Tests/UserTimeZoneTest.php index 09513fd..3229d31 100644 --- a/core/modules/user/src/Tests/UserTimeZoneTest.php +++ b/core/modules/user/src/Tests/UserTimeZoneTest.php @@ -2,6 +2,7 @@ namespace Drupal\user\Tests; +use Drupal\Core\Datetime\Entity\DateFormat; use Drupal\simpletest\WebTestBase; /** @@ -27,7 +28,7 @@ function testUserTimeZone() { ->set('timezone.user.configurable', 1) ->set('timezone.default', 'America/Los_Angeles') ->save(); - entity_load('date_format', 'medium') + DateFormat::load('medium') ->setPattern('Y-m-d H:i T') ->save(); diff --git a/core/modules/user/src/Tests/Views/BulkFormTest.php b/core/modules/user/src/Tests/Views/BulkFormTest.php index a4ff23d..c617c17 100644 --- a/core/modules/user/src/Tests/Views/BulkFormTest.php +++ b/core/modules/user/src/Tests/Views/BulkFormTest.php @@ -2,6 +2,7 @@ namespace Drupal\user\Tests\Views; +use Drupal\user\Entity\User; use Drupal\user\RoleInterface; use Drupal\views\Views; @@ -130,7 +131,7 @@ public function testBulkForm() { */ public function testBulkFormCombineFilter() { // Add a user. - $account = entity_load('user', $this->users[0]->id()); + User::load($this->users[0]->id()); $view = Views::getView('test_user_bulk_form_combine_filter'); $errors = $view->validate(); $this->assertEqual(reset($errors['default']), t('Field %field set in %filter is not usable for this filter type. Combined field filter only works for simple fields.', array('%field' => 'User: Bulk update', '%filter' => 'Global: Combine fields filter'))); diff --git a/core/modules/user/tests/src/Kernel/UserValidationTest.php b/core/modules/user/tests/src/Kernel/UserValidationTest.php index 251c810..b27b00f 100644 --- a/core/modules/user/tests/src/Kernel/UserValidationTest.php +++ b/core/modules/user/tests/src/Kernel/UserValidationTest.php @@ -48,6 +48,7 @@ function testUsernames() { 'foo@example.com' => array('Valid username', 'assertNull'), 'foo@-example.com' => array('Valid username', 'assertNull'), // invalid domains are allowed in usernames 'þòøÇߪř€' => array('Valid username', 'assertNull'), + 'foo+bar' => array('Valid username', 'assertNull'), // '+' symbol is allowed 'ᚠᛇᚻ᛫ᛒᛦᚦ' => array('Valid UTF8 username', 'assertNull'), // runes ' foo' => array('Invalid username that starts with a space', 'assertNotNull'), 'foo ' => array('Invalid username that ends with a space', 'assertNotNull'), diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 12adb4a..1394b23 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -232,7 +232,8 @@ function user_load($uid, $reset = FALSE) { * @see \Drupal\user\Entity\User::loadMultiple() */ function user_load_by_mail($mail) { - $users = entity_load_multiple_by_properties('user', array('mail' => $mail)); + $users = \Drupal::entityTypeManager()->getStorage('user') + ->loadByProperties(['mail' => $mail]); return $users ? reset($users) : FALSE; } @@ -248,7 +249,8 @@ function user_load_by_mail($mail) { * @see \Drupal\user\Entity\User::loadMultiple() */ function user_load_by_name($name) { - $users = entity_load_multiple_by_properties('user', array('name' => $name)); + $users = \Drupal::entityTypeManager()->getStorage('user') + ->loadByProperties(['name' => $name]); return $users ? reset($users) : FALSE; } @@ -984,7 +986,7 @@ function user_user_role_insert(RoleInterface $role) { } $add_id = 'user_add_role_action.' . $role->id(); - if (!entity_load('action', $add_id)) { + if (!Action::load($add_id)) { $action = Action::create(array( 'id' => $add_id, 'type' => 'user', @@ -997,7 +999,7 @@ function user_user_role_insert(RoleInterface $role) { $action->trustData()->save(); } $remove_id = 'user_remove_role_action.' . $role->id(); - if (!entity_load('action', $remove_id)) { + if (!Action::load($remove_id)) { $action = Action::create(array( 'id' => $remove_id, 'type' => 'user', @@ -1024,10 +1026,10 @@ function user_user_role_delete(RoleInterface $role) { return; } - $actions = entity_load_multiple('action', array( + $actions = Action::loadMultiple([ 'user_add_role_action.' . $role->id(), 'user_remove_role_action.' . $role->id(), - )); + ]); foreach ($actions as $action) { $action->delete(); } diff --git a/core/modules/views/src/Annotation/ViewsExposedForm.php b/core/modules/views/src/Annotation/ViewsExposedForm.php index 36ac058..a6f0fce 100644 --- a/core/modules/views/src/Annotation/ViewsExposedForm.php +++ b/core/modules/views/src/Annotation/ViewsExposedForm.php @@ -5,6 +5,7 @@ /** * Defines a Plugin annotation object for views exposed form plugins. * + * @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface * @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase * * @ingroup views_exposed_form_plugins diff --git a/core/modules/views/src/Form/ViewsExposedForm.php b/core/modules/views/src/Form/ViewsExposedForm.php index 36118c6..bea6152 100644 --- a/core/modules/views/src/Form/ViewsExposedForm.php +++ b/core/modules/views/src/Form/ViewsExposedForm.php @@ -114,7 +114,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $form['#theme'] = $view->buildThemeFunctions('views_exposed_form'); $form['#id'] = Html::cleanCssIdentifier('views_exposed_form-' . $view->storage->id() . '-' . $display['id']); - /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */ + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */ $exposed_form_plugin = $view->display_handler->getPlugin('exposed_form'); $exposed_form_plugin->exposedFormAlter($form, $form_state); @@ -137,7 +137,7 @@ public function validateForm(array &$form, FormStateInterface $form_state) { $handlers[$key]->validateExposed($form, $form_state); } } - /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */ + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */ $exposed_form_plugin = $view->display_handler->getPlugin('exposed_form'); $exposed_form_plugin->exposedFormValidate($form, $form_state); } @@ -159,7 +159,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $view->exposed_raw_input = []; $exclude = array('submit', 'form_build_id', 'form_id', 'form_token', 'exposed_form_plugin', 'reset'); - /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase $exposed_form_plugin */ + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */ $exposed_form_plugin = $view->display_handler->getPlugin('exposed_form'); $exposed_form_plugin->exposedFormSubmit($form, $form_state, $exclude); diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 6b2bc32..102718d 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -1339,6 +1339,7 @@ public function optionsSummary(&$categories, &$options) { ); } + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */ $exposed_form_plugin = $this->getPlugin('exposed_form'); if (!$exposed_form_plugin) { // Default to the no cache control plugin. @@ -2254,6 +2255,7 @@ public function preExecute() { } $this->view->initHandlers(); if ($this->usesExposed()) { + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */ $exposed_form = $this->getPlugin('exposed_form'); $exposed_form->preExecute(); } @@ -2550,6 +2552,7 @@ public function viewExposedFormBlocks() { $this->view->initHandlers(); if ($this->usesExposed() && $this->getOption('exposed_block')) { + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */ $exposed_form = $this->getPlugin('exposed_form'); return $exposed_form->renderExposedForm(TRUE); } diff --git a/core/modules/views/src/Plugin/views/display/Page.php b/core/modules/views/src/Plugin/views/display/Page.php index 8ca7f1c..a046698 100644 --- a/core/modules/views/src/Plugin/views/display/Page.php +++ b/core/modules/views/src/Plugin/views/display/Page.php @@ -293,7 +293,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#title' => $this->t('Show as expanded'), '#type' => 'checkbox', '#default_value' => !empty($menu['expanded']), - '#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded. '), + '#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded.'), ]; // Only display the parent selector if Menu UI module is enabled. @@ -519,7 +519,7 @@ public function getArgumentText() { public function getPagerText() { return array( 'items per page title' => $this->t('Items per page'), - 'items per page description' => $this->t('The number of items to display per page. Enter 0 for no limit.') + 'items per page description' => $this->t('Enter 0 for no limit.') ); } diff --git a/core/modules/views/src/Plugin/views/display/PathPluginBase.php b/core/modules/views/src/Plugin/views/display/PathPluginBase.php index da230c4..9ee7fd2 100644 --- a/core/modules/views/src/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/PathPluginBase.php @@ -237,8 +237,8 @@ public function alterRoutes(RouteCollection $collection) { $route_path = RouteCompiler::getPathWithoutDefaults($route); $route_path = RouteCompiler::getPatternOutline($route_path); // Ensure that we don't override a route which is already controlled by - // views. - if (!$route->hasDefault('view_id') && ('/' . $view_path == $route_path)) { + // views. Also ensure that we don't override for example REST routes. + if (!$route->hasDefault('view_id') && ('/' . $view_path == $route_path) && (!$route->getMethods() || in_array('GET', $route->getMethods()))) { $parameters = $route->compile()->getPathVariables(); // @todo Figure out whether we need to merge some settings (like diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php index e6325aa..62cb79c 100644 --- a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php +++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginBase.php @@ -10,31 +10,20 @@ use Drupal\views\Plugin\views\PluginBase; /** - * @defgroup views_exposed_form_plugins Views exposed form plugins - * @{ - * Plugins that handle validation, submission, and rendering of exposed forms. - * - * Exposed forms are used for filters, sorts, and pager settings that are - * exposed to site visitors. Exposed form plugins handle the rendering, - * validation, and submission of exposed forms, and may add additional form - * elements. - * - * Exposed form plugins extend - * \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase. They must be - * annotated with \Drupal\views\Annotation\ViewsExposedForm annotation, - * and they must be in namespace directory Plugin\views\exposed_form. - */ - -/** * Base class for Views exposed filter form plugins. + * + * @ingroup views_exposed_form_plugins */ -abstract class ExposedFormPluginBase extends PluginBase implements CacheableDependencyInterface { +abstract class ExposedFormPluginBase extends PluginBase implements CacheableDependencyInterface, ExposedFormPluginInterface { /** * {@inheritdoc} */ protected $usesOptions = TRUE; + /** + * {@inheritdoc} + */ protected function defineOptions() { $options = parent::defineOptions(); $options['submit_button'] = array('default' => $this->t('Apply')); @@ -47,6 +36,9 @@ protected function defineOptions() { return $options; } + /** + * {@inheritdoc} + */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); $form['submit_button'] = array( @@ -115,11 +107,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { } /** - * Render the exposed filter form. - * - * This actually does more than that; because it's using FAPI, the form will - * also assign data to the appropriate handlers for use in building the - * query. + * {@inheritdoc} */ public function renderExposedForm($block = FALSE) { // Deal with any exposed filters we may have, before building. @@ -154,6 +142,9 @@ public function renderExposedForm($block = FALSE) { } } + /** + * {@inheritdoc} + */ public function query() { $view = $this->view; $exposed_data = isset($view->exposed_data) ? $view->exposed_data : array(); @@ -179,21 +170,28 @@ public function query() { } } + /** + * {@inheritdoc} + */ public function preRender($values) { } + /** + * {@inheritdoc} + */ public function postRender(&$output) { } + /** + * {@inheritdoc} + */ public function preExecute() { } + /** + * {@inheritdoc} + */ public function postExecute() { } /** - * Alters the view exposed form. - * - * @param $form - * The form build array. Passed by reference. - * @param $form_state - * The form state. Passed by reference. + * {@inheritdoc} */ public function exposedFormAlter(&$form, FormStateInterface $form_state) { if (!empty($this->options['submit_button'])) { @@ -273,6 +271,9 @@ public function exposedFormAlter(&$form, FormStateInterface $form_state) { } } + /** + * {@inheritdoc} + */ public function exposedFormValidate(&$form, FormStateInterface $form_state) { if ($pager_plugin = $form_state->get('pager_plugin')) { $pager_plugin->exposedFormValidate($form, $form_state); @@ -280,15 +281,7 @@ public function exposedFormValidate(&$form, FormStateInterface $form_state) { } /** - * This function is executed when exposed form is submitted. - * - * @param $form - * Nested array of form elements that comprise the form. - * @param $form_state - * The current state of the form. - * @param $exclude - * Nested array of keys to exclude of insert into - * $view->exposed_raw_input + * {@inheritdoc} */ public function exposedFormSubmit(&$form, FormStateInterface $form_state, &$exclude) { if (!$form_state->isValueEmpty('op') && $form_state->getValue('op') == $this->options['reset_button_label']) { @@ -300,6 +293,17 @@ public function exposedFormSubmit(&$form, FormStateInterface $form_state, &$excl } } + /** + * Resets all the states of the exposed form. + * + * This method is called when the "Reset" button is triggered. Clears + * user inputs, stored session, and the form state. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ public function resetForm(&$form, FormStateInterface $form_state) { // _SESSION is not defined for users who are not logged in. @@ -373,7 +377,3 @@ public function getCacheTags() { } } - -/** - * @} - */ diff --git a/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginInterface.php b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginInterface.php new file mode 100644 index 0000000..9759ae5 --- /dev/null +++ b/core/modules/views/src/Plugin/views/exposed_form/ExposedFormPluginInterface.php @@ -0,0 +1,153 @@ +exposed_raw_input; for + * example, 'form_build_id'. + * + * @see \Drupal\views\Form\ViewsExposedForm::submitForm() + */ + public function exposedFormSubmit(&$form, FormStateInterface $form_state, &$exclude); + +} + +/** + * @} + */ diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 21803ce..cde5ecb 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -301,7 +301,7 @@ public function getElements() { if (!isset($elements)) { // @todo Add possible html5 elements. $elements = array( - '' => $this->t(' - Use default -'), + '' => $this->t('- Use default -'), '0' => $this->t('- None -') ); $elements += \Drupal::config('views.settings')->get('field_rewrite_elements'); diff --git a/core/modules/views/src/Plugin/views/filter/Combine.php b/core/modules/views/src/Plugin/views/filter/Combine.php index f5a6035..e13df51 100644 --- a/core/modules/views/src/Plugin/views/filter/Combine.php +++ b/core/modules/views/src/Plugin/views/filter/Combine.php @@ -99,22 +99,27 @@ public function query() { */ public function validate() { $errors = parent::validate(); - $fields = $this->view->display_handler->getHandlers('field'); - foreach ($this->options['fields'] as $id) { - if (!isset($fields[$id])) { - // Combined field filter only works with fields that are in the field - // settings. - $errors[] = $this->t('Field %field set in %filter is not set in this display.', array('%field' => $id, '%filter' => $this->adminLabel())); - break; - } - elseif (!$fields[$id]->clickSortable()) { - // Combined field filter only works with simple fields. If the field is - // not click sortable we can assume it is not a simple field. - // @todo change this check to isComputed. See - // https://www.drupal.org/node/2349465 - $errors[] = $this->t('Field %field set in %filter is not usable for this filter type. Combined field filter only works for simple fields.', array('%field' => $fields[$id]->adminLabel(), '%filter' => $this->adminLabel())); + if ($this->displayHandler->usesFields()) { + $fields = $this->displayHandler->getHandlers('field'); + foreach ($this->options['fields'] as $id) { + if (!isset($fields[$id])) { + // Combined field filter only works with fields that are in the field + // settings. + $errors[] = $this->t('Field %field set in %filter is not set in display %display.', array('%field' => $id, '%filter' => $this->adminLabel(), '%display' => $this->displayHandler->display['display_title'])); + break; + } + elseif (!$fields[$id]->clickSortable()) { + // Combined field filter only works with simple fields. If the field + // is not click sortable we can assume it is not a simple field. + // @todo change this check to isComputed. See + // https://www.drupal.org/node/2349465 + $errors[] = $this->t('Field %field set in %filter is not usable for this filter type. Combined field filter only works for simple fields.', array('%field' => $fields[$id]->adminLabel(), '%filter' => $this->adminLabel())); + } } } + else { + $errors[] = $this->t('%display: %filter can only be used on displays that use fields. Set the style or row format for that display to one using fields to use the combine field filter.', array('%display' => $this->displayHandler->display['display_title'], '%filter' => $this->adminLabel())); + } return $errors; } diff --git a/core/modules/views/src/Plugin/views/pager/SqlBase.php b/core/modules/views/src/Plugin/views/pager/SqlBase.php index 0c8a546..0154762 100644 --- a/core/modules/views/src/Plugin/views/pager/SqlBase.php +++ b/core/modules/views/src/Plugin/views/pager/SqlBase.php @@ -68,7 +68,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['total_pages'] = array( '#type' => 'number', '#title' => $this->t('Number of pages'), - '#description' => $this->t('The total number of pages. Leave empty to show all pages.'), + '#description' => $this->t('Leave empty to show all pages.'), '#default_value' => $this->options['total_pages'], ); diff --git a/core/modules/views/src/Plugin/views/query/Sql.php b/core/modules/views/src/Plugin/views/query/Sql.php index 321ebf5..cfe593c 100644 --- a/core/modules/views/src/Plugin/views/query/Sql.php +++ b/core/modules/views/src/Plugin/views/query/Sql.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Database; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\Core\Database\DatabaseExceptionWrapper; @@ -13,6 +14,7 @@ use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; use Drupal\views\Views; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Views query plugin for an SQL query. @@ -107,6 +109,40 @@ class Sql extends QueryPluginBase { protected $noDistinct; /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a Sql object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityTypeManager = $entity_type_manager; + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** * {@inheritdoc} */ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { @@ -1482,63 +1518,97 @@ public function loadEntities(&$results) { foreach ($entity_information as $info) { $entity_type = $info['entity_type']; if (!isset($entity_types[$entity_type])) { - $entity_types[$entity_type] = \Drupal::entityManager()->getDefinition($entity_type); + $entity_types[$entity_type] = $this->entityTypeManager->getDefinition($entity_type); } } // Assemble a list of entities to load. - $ids_by_type = array(); + $entity_ids_by_type = []; + $revision_ids_by_type = []; foreach ($entity_information as $info) { $relationship_id = $info['relationship_id']; $entity_type = $info['entity_type']; + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_info */ $entity_info = $entity_types[$entity_type]; - $id_key = !$info['revision'] ? $entity_info->getKey('id') : $entity_info->getKey('revision'); + $revision = $info['revision']; + $id_key = !$revision ? $entity_info->getKey('id') : $entity_info->getKey('revision'); $id_alias = $this->getFieldAlias($info['alias'], $id_key); foreach ($results as $index => $result) { // Store the entity id if it was found. if (isset($result->{$id_alias}) && $result->{$id_alias} != '') { - $ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias; + if ($revision) { + $revision_ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias; + } + else { + $entity_ids_by_type[$entity_type][$index][$relationship_id] = $result->$id_alias; + } } } } // Load all entities and assign them to the correct result row. - foreach ($ids_by_type as $entity_type => $ids) { + foreach ($entity_ids_by_type as $entity_type => $ids) { + $entity_storage = $this->entityTypeManager->getStorage($entity_type); $flat_ids = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator($ids)), FALSE); - // Drupal core currently has no way to load multiple revisions. Sad. - if (isset($entity_information[$entity_type]['revision']) && $entity_information[$entity_type]['revision'] === TRUE) { - $entities = array(); - foreach ($flat_ids as $revision_id) { - $entity = entity_revision_load($entity_type, $revision_id); - if ($entity) { - $entities[$revision_id] = $entity; - } + $entities = $entity_storage->loadMultiple(array_unique($flat_ids)); + $results = $this->assignEntitiesToResult($ids, $entities, $results); + } + + // Now load all revisions. + foreach ($revision_ids_by_type as $entity_type => $revision_ids) { + $entity_storage = $this->entityTypeManager->getStorage($entity_type); + $entities = []; + + foreach ($revision_ids as $index => $revision_id_by_relationship) { + foreach ($revision_id_by_relationship as $revision => $revision_id) { + // Drupal core currently has no way to load multiple revisions. + $entity = $entity_storage->loadRevision($revision_id); + $entities[$revision_id] = $entity; } } - else { - $entities = entity_load_multiple($entity_type, $flat_ids); - } - foreach ($ids as $index => $relationships) { - foreach ($relationships as $relationship_id => $entity_id) { - if (isset($entities[$entity_id])) { - $entity = $entities[$entity_id]; - } - else { - $entity = NULL; - } + $results = $this->assignEntitiesToResult($revision_ids, $entities, $results); + } + } - if ($relationship_id == 'none') { - $results[$index]->_entity = $entity; - } - else { - $results[$index]->_relationship_entities[$relationship_id] = $entity; - } + /** + * Sets entities onto the view result row objects. + * + * This method takes into account the relationship in which the entity was + * needed in the first place. + * + * @param mixed[][] $ids + * A two dimensional array of identifiers (entity ID / revision ID) keyed by + * relationship. + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * An array of entities keyed by their identified (entity ID / revision ID). + * @param \Drupal\views\ResultRow[] $results + * The entire views result. + * + * @return \Drupal\views\ResultRow[] + * The changed views results. + */ + protected function assignEntitiesToResult($ids, array $entities, array $results) { + foreach ($ids as $index => $relationships) { + foreach ($relationships as $relationship_id => $id) { + if (isset($entities[$id])) { + $entity = $entities[$id]; + } + else { + $entity = NULL; + } + + if ($relationship_id == 'none') { + $results[$index]->_entity = $entity; + } + else { + $results[$index]->_relationship_entities[$relationship_id] = $entity; } } } + return $results; } /** diff --git a/core/modules/views/src/Plugin/views/sort/Random.php b/core/modules/views/src/Plugin/views/sort/Random.php index 11e216b..c9a552f 100644 --- a/core/modules/views/src/Plugin/views/sort/Random.php +++ b/core/modules/views/src/Plugin/views/sort/Random.php @@ -3,6 +3,7 @@ namespace Drupal\views\Plugin\views\sort; use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Cache\UncacheableDependencyTrait; use Drupal\Core\Form\FormStateInterface; /** @@ -12,6 +13,8 @@ */ class Random extends SortPluginBase implements CacheableDependencyInterface { + use UncacheableDependencyTrait; + /** * {@inheritdoc} */ @@ -28,25 +31,4 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['order']['#access'] = FALSE; } - /** - * {@inheritdoc} - */ - public function getCacheMaxAge() { - return 0; - } - - /** - * {@inheritdoc} - */ - public function getCacheContexts() { - return []; - } - - /** - * {@inheritdoc} - */ - public function getCacheTags() { - return []; - } - } diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php index 404de3e..3bee96a 100644 --- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php +++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php @@ -345,7 +345,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['default_row_class'] = array( '#title' => $this->t('Add views row classes'), - '#description' => $this->t('Add the default row classes like views-row-1 to the output. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.'), + '#description' => $this->t('Add the default row classes like @classes to the output. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.', array('@classes' => 'views-row')), '#type' => 'checkbox', '#default_value' => $this->options['default_row_class'], ); diff --git a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php index 719d4da..c5cdd08 100644 --- a/core/modules/views/src/Tests/Plugin/ExposedFormTest.php +++ b/core/modules/views/src/Tests/Plugin/ExposedFormTest.php @@ -8,6 +8,7 @@ use Drupal\views\Tests\ViewTestBase; use Drupal\views\ViewExecutable; use Drupal\views\Views; +use Drupal\views\Entity\View; /** * Tests exposed forms functionality. @@ -241,7 +242,7 @@ public function testExposedBlock() { * Test the input required exposed form type. */ public function testInputRequired() { - $view = entity_load('view', 'test_exposed_form_buttons'); + $view = View::load('test_exposed_form_buttons'); $display = &$view->getDisplay('default'); $display['display_options']['exposed_form']['type'] = 'input_required'; $view->save(); diff --git a/core/modules/views/src/Tests/Plugin/StyleTableTest.php b/core/modules/views/src/Tests/Plugin/StyleTableTest.php index ade5ebe..9a3f25e 100644 --- a/core/modules/views/src/Tests/Plugin/StyleTableTest.php +++ b/core/modules/views/src/Tests/Plugin/StyleTableTest.php @@ -2,6 +2,8 @@ namespace Drupal\views\Tests\Plugin; +use Drupal\views\Entity\View; + /** * Tests the table style views plugin. * @@ -44,7 +46,7 @@ public function testAccessibilitySettings() { $this->assertEqual(trim((string) $result[0]), 'description-text'); // Remove the caption and ensure the caption is not displayed anymore. - $view = entity_load('view', 'test_table'); + $view = View::load('test_table'); $display = &$view->getDisplay('default'); $display['display_options']['style']['options']['caption'] = ''; $view->save(); @@ -88,7 +90,7 @@ public function testFieldInColumns() { $this->assertTrue(count($result), 'Ensure there is a td with the class views-field-job-1'); // Combine the second job-column with the first one, with ', ' as separator. - $view = entity_load('view', 'test_table'); + $view = View::load('test_table'); $display = &$view->getDisplay('default'); $display['display_options']['style']['options']['columns']['job_1'] = 'job'; $display['display_options']['style']['options']['info']['job']['separator'] = ', '; diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index cba3948..da85925 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -1220,6 +1220,7 @@ public function build($display_id = NULL) { $this->_preQuery(); if ($this->display_handler->usesExposed()) { + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */ $exposed_form = $this->display_handler->getPlugin('exposed_form'); $this->exposed_widgets = $exposed_form->renderExposedForm(); if (FormState::hasAnyErrors() || !empty($this->build_info['abort'])) { @@ -1447,6 +1448,7 @@ public function render($display_id = NULL) { return; } + /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */ $exposed_form = $this->display_handler->getPlugin('exposed_form'); $exposed_form->preRender($this->result); diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.base_and_revision.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.base_and_revision.yml new file mode 100644 index 0000000..d7d6162 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.base_and_revision.yml @@ -0,0 +1,346 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: base_and_revision +label: base_and_revision +module: views +description: '' +tag: '' +base_table: node_field_revision +base_field: vid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'view all revisions' + cache: + type: none + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + nid: + id: nid + table: node_field_revision + field: nid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: nid + plugin_id: field + vid: + id: vid + table: node_field_revision + field: vid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: vid + plugin_id: field + vid_1: + id: vid_1 + table: node_field_data + field: vid + relationship: nid + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: vid + plugin_id: field + filters: + vid: + id: vid + table: node_field_revision + field: vid + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: + min: '' + max: '' + value: '3' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: vid + plugin_id: numeric + sorts: { } + header: { } + footer: { } + empty: { } + relationships: + nid: + id: nid + table: node_field_revision + field: nid + relationship: none + group_type: group + admin_label: Node + required: false + entity_type: node + entity_field: nid + plugin_id: standard + arguments: { } + display_extenders: { } + rendering_language: en + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterCombineTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterCombineTest.php index d3aa2c9..775015b 100644 --- a/core/modules/views/tests/src/Kernel/Handler/FilterCombineTest.php +++ b/core/modules/views/tests/src/Kernel/Handler/FilterCombineTest.php @@ -13,11 +13,16 @@ class FilterCombineTest extends ViewsKernelTestBase { /** + * {@inheritdoc} + */ + public static $modules = array('entity_test'); + + /** * Views used by this test. * * @var array */ - public static $testViews = array('test_view'); + public static $testViews = array('test_view', 'entity_test_fields'); /** * Map column names. @@ -29,6 +34,15 @@ class FilterCombineTest extends ViewsKernelTestBase { 'views_test_data_job' => 'job', ); + /** + * {@inheritdoc} + */ + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); + + $this->installEntitySchema('entity_test'); + } + public function testFilterCombineContains() { $view = Views::getView('test_view'); $view->setDisplay(); @@ -126,7 +140,42 @@ public function testFilterCombineContainsFieldsOverwritten() { $this->assertTrue($view->build_info['fail'], "View build has been marked as failed."); // Make sure this view does not pass validation with the right error. $errors = $view->validate(); - $this->assertEqual(reset($errors['default']), t('Field %field set in %filter is not set in this display.', array('%field' => 'dummy', '%filter' => 'Global: Combine fields filter'))); + $this->assertEquals(t('Field %field set in %filter is not set in display %display.', array('%field' => 'dummy', '%filter' => 'Global: Combine fields filter', '%display' => 'Master')), reset($errors['default'])); + } + + /** + * Tests that the combine field filter is not valid on displays that don't use + * fields. + */ + public function testNonFieldsRow() { + $view = Views::getView('entity_test_fields'); + $view->setDisplay(); + + // Set the rows to a plugin type that doesn't support fields. + $view->displayHandlers->get('default')->overrideOption('row', array( + 'type' => 'entity:entity_test', + 'options' => array( + 'view_mode' => 'teaser', + ), + )); + // Change the filtering. + $view->displayHandlers->get('default')->overrideOption('filters', array( + 'name' => array( + 'id' => 'combine', + 'table' => 'views', + 'field' => 'combine', + 'relationship' => 'none', + 'operator' => 'contains', + 'fields' => array( + 'name', + ), + 'value' => 'ing', + ), + )); + $this->executeView($view); + $errors = $view->validate(); + // Check that the right error is shown. + $this->assertEquals(t('%display: %filter can only be used on displays that use fields. Set the style or row format for that display to one using fields to use the combine field filter.', array('%filter' => 'Global: Combine fields filter', '%display' => 'Master')), reset($errors['default'])); } /** diff --git a/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php b/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php index 6aeacf1..156b061 100644 --- a/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php +++ b/core/modules/views/tests/src/Kernel/Plugin/DisplayKernelTest.php @@ -6,7 +6,7 @@ use Drupal\Tests\views\Kernel\ViewsKernelTestBase; use Drupal\views\Plugin\views\style\StylePluginBase; use Drupal\views\Plugin\views\access\AccessPluginBase; -use Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase; +use Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface; use Drupal\views\Plugin\views\pager\PagerPluginBase; use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\Plugin\views\cache\CachePluginBase; @@ -96,7 +96,7 @@ public function testGetPlugin() { $this->assertTrue($display_handler->getPlugin('access') instanceof AccessPluginBase, 'An access plugin instance was returned.'); $this->assertTrue($display_handler->getPlugin('cache') instanceof CachePluginBase, 'A cache plugin instance was returned.'); - $this->assertTrue($display_handler->getPlugin('exposed_form') instanceof ExposedFormPluginBase, 'An exposed_form plugin instance was returned.'); + $this->assertTrue($display_handler->getPlugin('exposed_form') instanceof ExposedFormPluginInterface, 'An exposed_form plugin instance was returned.'); $this->assertTrue($display_handler->getPlugin('pager') instanceof PagerPluginBase, 'A pager plugin instance was returned.'); $this->assertTrue($display_handler->getPlugin('query') instanceof QueryPluginBase, 'A query plugin instance was returned.'); $this->assertTrue($display_handler->getPlugin('row') instanceof RowPluginBase, 'A row plugin instance was returned.'); diff --git a/core/modules/views/tests/src/Kernel/Plugin/SqlEntityLoadingTest.php b/core/modules/views/tests/src/Kernel/Plugin/SqlEntityLoadingTest.php new file mode 100644 index 0000000..339638c --- /dev/null +++ b/core/modules/views/tests/src/Kernel/Plugin/SqlEntityLoadingTest.php @@ -0,0 +1,83 @@ +installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('node', 'node_access'); + } + + public function testViewWithNonDefaultForwardRevision() { + $node_type = NodeType::create([ + 'type' => 'page', + ]); + $node_type->save(); + + $node = Node::create([ + 'type' => 'page', + 'title' => 'test title', + ]); + $node->save(); + + // Creates the first revision, which is set as default. + $revision = clone $node; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(TRUE); + $revision->save(); + + // Creates the second revision, which is not set as default. + $revision2 = clone $node; + $revision2->setNewRevision(TRUE); + $revision2->isDefaultRevision(FALSE); + $revision2->save(); + + $view = Views::getView('base_and_revision'); + $view->execute(); + + $expected = [ + [ + 'nid' => $node->id(), + // The default revision ID. + 'vid_1' => $revision->getRevisionId(), + // THe latest revision ID. + 'vid' => $revision2->getRevisionId(), + ], + ]; + $this->assertIdenticalResultset($view, $expected, [ + 'node_field_data_node_field_revision_nid' => 'nid', + 'vid_1' => 'vid_1', + 'vid' => 'vid', + ]); + } + +} diff --git a/core/modules/views/tests/src/Kernel/ViewExecutableTest.php b/core/modules/views/tests/src/Kernel/ViewExecutableTest.php index 0ca5587..01057b4 100644 --- a/core/modules/views/tests/src/Kernel/ViewExecutableTest.php +++ b/core/modules/views/tests/src/Kernel/ViewExecutableTest.php @@ -99,7 +99,7 @@ protected function setUpFixtures() { public function testFactoryService() { $factory = $this->container->get('views.executable'); $this->assertTrue($factory instanceof ViewExecutableFactory, 'A ViewExecutableFactory instance was returned from the container.'); - $view = entity_load('view', 'test_executable_displays'); + $view = View::load('test_executable_displays'); $this->assertTrue($factory->get($view) instanceof ViewExecutable, 'A ViewExecutable instance was returned from the factory.'); } diff --git a/core/modules/views/tests/src/Kernel/ViewStorageTest.php b/core/modules/views/tests/src/Kernel/ViewStorageTest.php index 5e999e0..fa264bd 100644 --- a/core/modules/views/tests/src/Kernel/ViewStorageTest.php +++ b/core/modules/views/tests/src/Kernel/ViewStorageTest.php @@ -78,7 +78,7 @@ function testConfigurationEntityCRUD() { * Tests loading configuration entities. */ protected function loadTests() { - $view = entity_load('view', 'test_view_storage'); + $view = View::load('test_view_storage'); $data = $this->config('views.view.test_view_storage')->get(); // Confirm that an actual view object is loaded and that it returns all of @@ -143,7 +143,7 @@ protected function createTests() { // Check the UUID of the loaded View. $created->save(); - $created_loaded = entity_load('view', 'test_view_storage_new'); + $created_loaded = View::load('test_view_storage_new'); $this->assertIdentical($created->uuid(), $created_loaded->uuid(), 'The created UUID has been saved correctly.'); } @@ -152,7 +152,7 @@ protected function createTests() { */ protected function displayTests() { // Check whether a display can be added and saved to a View. - $view = entity_load('view', 'test_view_storage_new'); + $view = View::load('test_view_storage_new'); $new_id = $view->addDisplay('page', 'Test', 'test'); $display = $view->get('display'); diff --git a/core/modules/views/tests/src/Unit/Plugin/display/PathPluginBaseTest.php b/core/modules/views/tests/src/Unit/Plugin/display/PathPluginBaseTest.php index 90d2ae7..9241a35 100644 --- a/core/modules/views/tests/src/Unit/Plugin/display/PathPluginBaseTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/display/PathPluginBaseTest.php @@ -261,6 +261,44 @@ public function testAlterRoute() { } /** + * Tests the altering of a REST route. + */ + public function testAlterRestRoute() { + $collection = new RouteCollection(); + $route = new Route('test_route', ['_controller' => 'Drupal\Tests\Core\Controller\TestController::content']); + $route->setMethods(['POST']); + $collection->add('test_route', $route); + + list($view) = $this->setupViewExecutableAccessPlugin(); + + $display = []; + $display['display_plugin'] = 'page'; + $display['id'] = 'page_1'; + $display['display_options'] = [ + 'path' => 'test_route', + ]; + $this->pathPlugin->initDisplay($view, $display); + + $this->pathPlugin->collectRoutes($collection); + $view_route_names = $this->pathPlugin->alterRoutes($collection); + $this->assertEquals([], $view_route_names); + + // Ensure that the test_route is not overridden. + $this->assertCount(2, $collection); + $route = $collection->get('test_route'); + $this->assertTrue($route instanceof Route); + $this->assertFalse($route->hasDefault('view_id')); + $this->assertFalse($route->hasDefault('display_id')); + $this->assertSame($collection->get('test_route'), $route); + + $route = $collection->get('view.test_id.page_1'); + $this->assertTrue($route instanceof Route); + $this->assertEquals('test_id', $route->getDefault('view_id')); + $this->assertEquals('page_1', $route->getDefault('display_id')); + $this->assertEquals('my views title', $route->getDefault('_title')); + } + + /** * Tests the alter route method with preexisting title callback. */ public function testAlterRouteWithAlterCallback() { diff --git a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php index ae06eaf..4958846 100644 --- a/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/query/SqlTest.php @@ -2,9 +2,18 @@ namespace Drupal\Tests\views\Unit\Plugin\query; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityType; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Tests\UnitTestCase; use Drupal\views\Plugin\views\query\Sql; +use Drupal\views\Plugin\views\relationship\RelationshipPluginBase; use Drupal\views\ResultRow; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewExecutable; +use Drupal\views\ViewsData; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @coversDefaultClass \Drupal\views\Plugin\views\query\Sql @@ -19,8 +28,9 @@ class SqlTest extends UnitTestCase { */ public function testGetCacheTags() { $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal(); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $query = new Sql([], 'sql', []); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); $query->view = $view; $result = []; @@ -64,8 +74,9 @@ public function testGetCacheTags() { */ public function testGetCacheMaxAge() { $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal(); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $query = new Sql([], 'sql', []); + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); $query->view = $view; $view->result = []; @@ -98,4 +109,427 @@ public function testGetCacheMaxAge() { $this->assertEquals(10, $query->getCacheMaxAge()); } + /** + * Sets up the views data in the container. + * + * @param \Drupal\views\ViewsData $views_data + * The views data. + */ + protected function setupViewsData(ViewsData $views_data) { + $container = \Drupal::hasContainer() ? \Drupal::getContainer() : new ContainerBuilder(); + $container->set('views.views_data', $views_data); + \Drupal::setContainer($container); + } + + /** + * Sets up the entity type manager in the container. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + protected function setupEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) { + $container = \Drupal::hasContainer() ? \Drupal::getContainer() : new ContainerBuilder(); + $container->set('entity_type.manager', $entity_type_manager); + $container->set('entity.manager', $entity_type_manager); + \Drupal::setContainer($container); + } + + /** + * Sets up some test entity types and corresponding views data. + * + * @param \Drupal\Core\Entity\EntityInterface[][] $entities_by_type + * Test entities keyed by entity type and entity ID. + * @param \Drupal\Core\Entity\EntityInterface[][] $entities_by_type + * Test entities keyed by entity type and revision ID. + * + * @return \Prophecy\Prophecy\ObjectProphecy + */ + protected function setupEntityTypes($entities_by_type = [], $entity_revisions_by_type = []) { + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type0 = new EntityType([ + 'label' => 'First', + 'id' => 'first', + 'base_table' => 'entity_first', + 'revision_table' => 'entity_first__revision', + 'entity_keys' => [ + 'id' => 'id', + 'revision' => 'vid', + ], + ]); + $entity_type1 = new EntityType([ + 'label' => 'second', + 'id' => 'second', + 'base_table' => 'entity_second', + 'revision_table' => 'entity_second__revision', + 'entity_keys' => [ + 'id' => 'id', + 'revision' => 'vid', + ], + ]); + + $entity_type_manager->getDefinitions()->willReturn([ + 'first' => $entity_type0, + 'second' => $entity_type1, + 'base_table' => 'entity_second', + ]); + + $entity_type_manager->getDefinition('first')->willReturn($entity_type0); + $entity_type_manager->getDefinition('second')->willReturn($entity_type1); + + // Setup the views data corresponding to the entity types. + $views_data = $this->prophesize(ViewsData::class); + $views_data->get('entity_first')->willReturn([ + 'table' => [ + 'entity type' => 'first', + 'entity revision' => FALSE, + ], + ]); + $views_data->get('entity_first__revision')->willReturn([ + 'table' => [ + 'entity type' => 'first', + 'entity revision' => TRUE, + ], + ]); + $views_data->get('entity_second')->willReturn([ + 'table' => [ + 'entity type' => 'second', + 'entity revision' => FALSE, + ], + ]); + $views_data->get('entity_second__revision')->willReturn([ + 'table' => [ + 'entity type' => 'second', + 'entity revision' => TRUE, + ], + ]); + $this->setupViewsData($views_data->reveal()); + + // Setup the loading of entities and entity revisions. + $entity_storages = [ + 'first' => $this->prophesize(EntityStorageInterface::class), + 'second' => $this->prophesize(EntityStorageInterface::class), + ]; + + foreach ($entities_by_type as $entity_type_id => $entities) { + foreach ($entities as $entity_id => $entity) { + $entity_storages[$entity_type_id]->load($entity_id)->willReturn($entity); + } + $entity_storages[$entity_type_id]->loadMultiple(array_keys($entities))->willReturn($entities); + } + + foreach ($entity_revisions_by_type as $entity_type_id => $entity_revisions) { + foreach ($entity_revisions as $revision_id => $revision) { + $entity_storages[$entity_type_id]->loadRevision($revision_id)->willReturn($revision); + } + } + + $entity_type_manager->getStorage('first')->willReturn($entity_storages['first']); + $entity_type_manager->getStorage('second')->willReturn($entity_storages['second']); + + $this->setupEntityTypeManager($entity_type_manager->reveal()); + + return $entity_type_manager; + } + + /** + * @covers ::loadEntities + * @covers ::assignEntitiesToResult + */ + public function testLoadEntitiesWithEmptyResult() { + $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal(); + $view_entity = $this->prophesize(ViewEntityInterface::class); + $view_entity->get('base_table')->willReturn('entity_first'); + $view_entity->get('base_field')->willReturn('id'); + $view->storage = $view_entity->reveal(); + + $entity_type_manager = $this->setupEntityTypes(); + + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query->view = $view; + + $result = []; + $query->addField('entity_first', 'id', 'id'); + $query->loadEntities($result); + $this->assertEmpty($result); + } + + /** + * @covers ::loadEntities + * @covers ::assignEntitiesToResult + */ + public function testLoadEntitiesWithNoRelationshipAndNoRevision() { + $view = $this->prophesize('Drupal\views\ViewExecutable')->reveal(); + $view_entity = $this->prophesize(ViewEntityInterface::class); + $view_entity->get('base_table')->willReturn('entity_first'); + $view_entity->get('base_field')->willReturn('id'); + $view->storage = $view_entity->reveal(); + + $entities = [ + 'first' => [ + 1 => $this->prophesize(EntityInterface::class)->reveal(), + 2 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_type_manager = $this->setupEntityTypes($entities); + + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query->view = $view; + + $result = []; + $result[] = new ResultRow([ + 'id' => 1, + ]); + // Note: Let the same entity be returned multiple times, for example to + // support the translation usecase. + $result[] = new ResultRow([ + 'id' => 2, + ]); + $result[] = new ResultRow([ + 'id' => 2, + ]); + + $query->addField('entity_first', 'id', 'id'); + $query->loadEntities($result); + + $this->assertSame($entities['first'][1], $result[0]->_entity); + $this->assertSame($entities['first'][2], $result[1]->_entity); + $this->assertSame($entities['first'][2], $result[2]->_entity); + } + + /** + * Create a view with a relationship. + */ + protected function setupViewWithRelationships(ViewExecutable $view, $base = 'entity_second') { + // We don't use prophecy, because prophecy enforces methods. + $relationship = $this->getMockBuilder(RelationshipPluginBase::class)->disableOriginalConstructor()->getMock(); + $relationship->definition['base'] = $base; + $relationship->tableAlias = $base; + $relationship->alias = $base; + + $view->relationship[$base] = $relationship; + } + + /** + * @covers ::loadEntities + * @covers ::assignEntitiesToResult + */ + public function testLoadEntitiesWithRelationship() { + // We don't use prophecy, because prophecy enforces methods. + $view = $this->getMockBuilder(ViewExecutable::class)->disableOriginalConstructor()->getMock(); + $this->setupViewWithRelationships($view); + + $view_entity = $this->prophesize(ViewEntityInterface::class); + $view_entity->get('base_table')->willReturn('entity_first'); + $view_entity->get('base_field')->willReturn('id'); + $view->storage = $view_entity->reveal(); + + $entities = [ + 'first' => [ + 1 => $this->prophesize(EntityInterface::class)->reveal(), + 2 => $this->prophesize(EntityInterface::class)->reveal(), + ], + 'second' => [ + 11 => $this->prophesize(EntityInterface::class)->reveal(), + 12 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_type_manager = $this->setupEntityTypes($entities); + + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query->view = $view; + + $result = []; + $result[] = new ResultRow([ + 'id' => 1, + 'entity_second__id' => 11, + ]); + // Provide an explicit NULL value, to test the case of a non required + // relationship. + $result[] = new ResultRow([ + 'id' => 2, + 'entity_second__id' => NULL, + ]); + $result[] = new ResultRow([ + 'id' => 2, + 'entity_second__id' => 12, + ]); + + $query->addField('entity_first', 'id', 'id'); + $query->addField('entity_second', 'id', 'entity_second__id'); + $query->loadEntities($result); + + $this->assertSame($entities['first'][1], $result[0]->_entity); + $this->assertSame($entities['first'][2], $result[1]->_entity); + $this->assertSame($entities['first'][2], $result[2]->_entity); + + $this->assertSame($entities['second'][11], $result[0]->_relationship_entities['entity_second']); + $this->assertEquals([], $result[1]->_relationship_entities); + $this->assertSame($entities['second'][12], $result[2]->_relationship_entities['entity_second']); + } + + /** + * @covers ::loadEntities + * @covers ::assignEntitiesToResult + */ + public function testLoadEntitiesWithRevision() { + // We don't use prophecy, because prophecy enforces methods. + $view = $this->getMockBuilder(ViewExecutable::class) + ->disableOriginalConstructor() + ->getMock(); + + $view_entity = $this->prophesize(ViewEntityInterface::class); + $view_entity->get('base_table')->willReturn('entity_first__revision'); + $view_entity->get('base_field')->willReturn('vid'); + $view->storage = $view_entity->reveal(); + + $entity_revisions = [ + 'first' => [ + 1 => $this->prophesize(EntityInterface::class)->reveal(), + 3 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_type_manager = $this->setupEntityTypes([], $entity_revisions); + + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query->view = $view; + + $result = []; + $result[] = new ResultRow([ + 'vid' => 1, + ]); + $result[] = new ResultRow([ + 'vid' => 1, + ]); + $result[] = new ResultRow([ + 'vid' => 3, + ]); + + $query->addField('entity_first__revision', 'vid', 'vid'); + $query->loadEntities($result); + + $this->assertSame($entity_revisions['first'][1], $result[0]->_entity); + $this->assertSame($entity_revisions['first'][1], $result[1]->_entity); + $this->assertSame($entity_revisions['first'][3], $result[2]->_entity); + } + + /** + * @covers ::loadEntities + * @covers ::assignEntitiesToResult + */ + public function testLoadEntitiesWithRevisionOfSameEntityType() { + // We don't use prophecy, because prophecy enforces methods. + $view = $this->getMockBuilder(ViewExecutable::class) + ->disableOriginalConstructor() + ->getMock(); + $this->setupViewWithRelationships($view, 'entity_first__revision'); + + $view_entity = $this->prophesize(ViewEntityInterface::class); + $view_entity->get('base_table')->willReturn('entity_first'); + $view_entity->get('base_field')->willReturn('id'); + $view->storage = $view_entity->reveal(); + + $entity = [ + 'first' => [ + 1 => $this->prophesize(EntityInterface::class)->reveal(), + 2 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_revisions = [ + 'first' => [ + 1 => $this->prophesize(EntityInterface::class)->reveal(), + 2 => $this->prophesize(EntityInterface::class)->reveal(), + 3 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_type_manager = $this->setupEntityTypes($entity, $entity_revisions); + + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query->view = $view; + + $result = []; + $result[] = new ResultRow([ + 'id' => 1, + 'entity_first__revision__vid' => 1, + ]); + $result[] = new ResultRow([ + 'id' => 2, + 'entity_first__revision__vid' => 2, + ]); + $result[] = new ResultRow([ + 'id' => 2, + 'entity_first__revision__vid' => 3, + ]); + + $query->addField('entity_first', 'id', 'id'); + $query->addField('entity_first__revision', 'vid', 'entity_first__revision__vid'); + $query->loadEntities($result); + + $this->assertSame($entity['first'][1], $result[0]->_entity); + $this->assertSame($entity['first'][2], $result[1]->_entity); + $this->assertSame($entity['first'][2], $result[2]->_entity); + $this->assertSame($entity_revisions['first'][1], $result[0]->_relationship_entities['entity_first__revision']); + $this->assertSame($entity_revisions['first'][2], $result[1]->_relationship_entities['entity_first__revision']); + $this->assertSame($entity_revisions['first'][3], $result[2]->_relationship_entities['entity_first__revision']); + } + + /** + * @covers ::loadEntities + * @covers ::assignEntitiesToResult + */ + public function testLoadEntitiesWithRelationshipAndRevision() { + // We don't use prophecy, because prophecy enforces methods. + $view = $this->getMockBuilder(ViewExecutable::class)->disableOriginalConstructor()->getMock(); + $this->setupViewWithRelationships($view); + + $view_entity = $this->prophesize(ViewEntityInterface::class); + $view_entity->get('base_table')->willReturn('entity_first__revision'); + $view_entity->get('base_field')->willReturn('vid'); + $view->storage = $view_entity->reveal(); + + $entities = [ + 'second' => [ + 11 => $this->prophesize(EntityInterface::class)->reveal(), + 12 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_revisions = [ + 'first' => [ + 1 => $this->prophesize(EntityInterface::class)->reveal(), + 3 => $this->prophesize(EntityInterface::class)->reveal(), + ], + ]; + $entity_type_manager = $this->setupEntityTypes($entities, $entity_revisions); + + $query = new Sql([], 'sql', [], $entity_type_manager->reveal()); + $query->view = $view; + + $result = []; + $result[] = new ResultRow([ + 'vid' => 1, + 'entity_second__id' => 11, + ]); + // Provide an explicit NULL value, to test the case of a non required + // relationship. + $result[] = new ResultRow([ + 'vid' => 1, + 'entity_second__id' => NULL, + ]); + $result[] = new ResultRow([ + 'vid' => 3, + 'entity_second__id' => 12, + ]); + + $query->addField('entity_first__revision', 'vid', 'vid'); + $query->addField('entity_second', 'id', 'entity_second__id'); + $query->loadEntities($result); + + $this->assertSame($entity_revisions['first'][1], $result[0]->_entity); + $this->assertSame($entity_revisions['first'][1], $result[1]->_entity); + $this->assertSame($entity_revisions['first'][3], $result[2]->_entity); + + $this->assertSame($entities['second'][11], $result[0]->_relationship_entities['entity_second']); + $this->assertEquals([], $result[1]->_relationship_entities); + $this->assertSame($entities['second'][12], $result[2]->_relationship_entities['entity_second']); + } + } diff --git a/core/modules/views/views.install b/core/modules/views/views.install index 1a6f3ff..8c5041c 100644 --- a/core/modules/views/views.install +++ b/core/modules/views/views.install @@ -370,3 +370,19 @@ function views_update_8100() { /** * @} End of "addtogroup updates-8.1.0". */ + +/** + * @addtogroup updates-8.2.0 + * @{ + */ + +/** + * Rebuild the container to add a new container parameter. + */ +function views_update_8200() { + // Empty update to cause a cache rebuild so that the container is rebuilt. +} + +/** + * @} End of "addtogroup updates-8.2.0". + */ diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 2d670c2..e82b6c8 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -182,3 +182,26 @@ function views_post_update_taxonomy_index_tid() { /** * @} End of "addtogroup updates-8.1.x". */ + +/** + * @addtogroup updates-8.2.x + * @{ + */ + +/** + * Fix views with serializer dependencies. + */ +function views_post_update_serializer_dependencies() { + $views = View::loadMultiple(); + array_walk($views, function(View $view) { + $old_dependencies = $view->getDependencies(); + $new_dependencies = $view->calculateDependencies()->getDependencies(); + if ($old_dependencies !== $new_dependencies) { + $view->save(); + } + }); +} + +/** + * @} End of "addtogroup updates-8.2.x". + */ diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc index abbbf31..aca4b8a 100644 --- a/core/modules/views/views.views.inc +++ b/core/modules/views/views.views.inc @@ -114,7 +114,7 @@ function views_views_data() { $data['views']['combine'] = array( 'title' => t('Combine fields filter'), - 'help' => t('Combine two fields together and search by them.'), + 'help' => t('Combine multiple fields together and search by them.'), 'filter' => array( 'id' => 'combine', ), diff --git a/core/modules/views_ui/src/Form/Ajax/AddHandler.php b/core/modules/views_ui/src/Form/Ajax/AddHandler.php index b63c024..ada0ccd 100644 --- a/core/modules/views_ui/src/Form/Ajax/AddHandler.php +++ b/core/modules/views_ui/src/Form/Ajax/AddHandler.php @@ -95,7 +95,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $groups = array('all' => $this->t('- All -')); $form['override']['controls']['group'] = array( '#type' => 'select', - '#title' => $this->t('Type'), + '#title' => $this->t('Category'), '#options' => array(), ); diff --git a/core/modules/views_ui/src/Tests/DisplayTest.php b/core/modules/views_ui/src/Tests/DisplayTest.php index 39b85e4..7f5f125 100644 --- a/core/modules/views_ui/src/Tests/DisplayTest.php +++ b/core/modules/views_ui/src/Tests/DisplayTest.php @@ -191,7 +191,7 @@ public function testLinkDisplay() { */ public function testPageContextualLinks() { $this->drupalLogin($this->drupalCreateUser(array('administer views', 'access contextual links'))); - $view = entity_load('view', 'test_display'); + $view = View::load('test_display'); $view->enable()->save(); $this->container->get('router.builder')->rebuildIfNeeded(); diff --git a/core/modules/views_ui/views_ui.permissions.yml b/core/modules/views_ui/views_ui.permissions.yml index fd49722..1c57779 100644 --- a/core/modules/views_ui/views_ui.permissions.yml +++ b/core/modules/views_ui/views_ui.permissions.yml @@ -1,4 +1,3 @@ administer views: title: 'Administer views' - description: 'Access the views administration pages.' restrict access: true diff --git a/core/phpcs.xml.dist b/core/phpcs.xml.dist index 3fcb48e..f860ff6 100644 --- a/core/phpcs.xml.dist +++ b/core/phpcs.xml.dist @@ -74,6 +74,11 @@ + + + + + diff --git a/core/profiles/standard/config/install/node.type.article.yml b/core/profiles/standard/config/install/node.type.article.yml index cc5f7b8..1fd439c 100644 --- a/core/profiles/standard/config/install/node.type.article.yml +++ b/core/profiles/standard/config/install/node.type.article.yml @@ -5,6 +5,6 @@ name: Article type: article description: 'Use articles for time-sensitive content like news, press releases or blog posts.' help: '' -new_revision: false +new_revision: true preview_mode: 1 display_submitted: true diff --git a/core/profiles/standard/config/install/node.type.page.yml b/core/profiles/standard/config/install/node.type.page.yml index 70bed48..57dcc0c 100644 --- a/core/profiles/standard/config/install/node.type.page.yml +++ b/core/profiles/standard/config/install/node.type.page.yml @@ -5,6 +5,6 @@ name: 'Basic page' type: page description: 'Use basic pages for your static content, such as an ''About us'' page.' help: '' -new_revision: false +new_revision: true preview_mode: 1 display_submitted: false diff --git a/core/rebuild.php b/core/rebuild.php index ccd4976..4e69eab 100644 --- a/core/rebuild.php +++ b/core/rebuild.php @@ -38,9 +38,9 @@ } if (Settings::get('rebuild_access', FALSE) || - ($request->get('token') && $request->get('timestamp') && - ((REQUEST_TIME - $request->get('timestamp')) < 300) && - Crypt::hashEquals(Crypt::hmacBase64($request->get('timestamp'), Settings::get('hash_salt')), $request->get('token')) + ($request->query->get('token') && $request->query->get('timestamp') && + ((REQUEST_TIME - $request->query->get('timestamp')) < 300) && + Crypt::hashEquals(Crypt::hmacBase64($request->query->get('timestamp'), Settings::get('hash_salt')), $request->query->get('token')) )) { // Clear the APCu cache to ensure APCu class loader is reset. if (function_exists('apcu_clear_cache')) { diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php new file mode 100644 index 0000000..72e7b6f --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php @@ -0,0 +1,480 @@ +container->get('router.builder')->rebuild(); + + $this->assetResolver = $this->container->get('asset.resolver'); + $this->renderer = $this->container->get('renderer'); + } + + /** + * Tests that default CSS and JavaScript is empty. + */ + function testDefault() { + $assets = new AttachedAssets(); + $this->assertEqual(array(), $this->assetResolver->getCssAssets($assets, FALSE), 'Default CSS is empty.'); + list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, FALSE); + $this->assertEqual(array(), $js_assets_header, 'Default header JavaScript is empty.'); + $this->assertEqual(array(), $js_assets_footer, 'Default footer JavaScript is empty.'); + } + + /** + * Tests non-existing libraries. + */ + function testLibraryUnknown() { + $build['#attached']['library'][] = 'core/unknown'; + $assets = AttachedAssets::createFromRenderArray($build); + + $this->assertIdentical([], $this->assetResolver->getJsAssets($assets, FALSE)[0], 'Unknown library was not added to the page.'); + } + + /** + * Tests adding a CSS and a JavaScript file. + */ + function testAddFiles() { + $build['#attached']['library'][] = 'common_test/files'; + $assets = AttachedAssets::createFromRenderArray($build); + + $css = $this->assetResolver->getCssAssets($assets, FALSE); + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/bar.css', $css), 'CSS files are correctly added.'); + $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/foo.js', $js), 'JavaScript files are correctly added.'); + + $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_css = $this->renderer->renderPlain($css_render_array); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'Rendering an external CSS file.'); + $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'Rendering an external JavaScript file.'); + } + + /** + * Tests adding JavaScript settings. + */ + function testAddJsSettings() { + // Add a file in order to test default settings. + $build['#attached']['library'][] = 'core/drupalSettings'; + $assets = AttachedAssets::createFromRenderArray($build); + + $this->assertEqual([], $assets->getSettings(), 'JavaScript settings on $assets are empty.'); + $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $this->assertTrue(array_key_exists('currentPath', $javascript['drupalSettings']['data']['path']), 'The current path JavaScript setting is set correctly.'); + $this->assertTrue(array_key_exists('currentPath', $assets->getSettings()['path']), 'JavaScript settings on $assets are resolved after retrieving JavaScript assets, and are equal to the returned JavaScript settings.'); + + $assets->setSettings(['drupal' => 'rocks', 'dries' => 280342800]); + $javascript = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $this->assertEqual(280342800, $javascript['drupalSettings']['data']['dries'], 'JavaScript setting is set correctly.'); + $this->assertEqual('rocks', $javascript['drupalSettings']['data']['drupal'], 'The other JavaScript setting is set correctly.'); + } + + /** + * Tests adding external CSS and JavaScript files. + */ + function testAddExternalFiles() { + $build['#attached']['library'][] = 'common_test/external'; + $assets = AttachedAssets::createFromRenderArray($build); + + $css = $this->assetResolver->getCssAssets($assets, FALSE); + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $this->assertTrue(array_key_exists('http://example.com/stylesheet.css', $css), 'External CSS files are correctly added.'); + $this->assertTrue(array_key_exists('http://example.com/script.js', $js), 'External JavaScript files are correctly added.'); + + $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_css = $this->renderer->renderPlain($css_render_array); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'Rendering an external CSS file.'); + $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'Rendering an external JavaScript file.'); + } + + /** + * Tests adding JavaScript files with additional attributes. + */ + function testAttributes() { + $build['#attached']['library'][] = 'common_test/js-attributes'; + $assets = AttachedAssets::createFromRenderArray($build); + + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $expected_1 = ''; + $expected_2 = ''; + $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.'); + $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.'); + } + + /** + * Tests that attributes are maintained when JS aggregation is enabled. + */ + function testAggregatedAttributes() { + $build['#attached']['library'][] = 'common_test/js-attributes'; + $assets = AttachedAssets::createFromRenderArray($build); + + $js = $this->assetResolver->getJsAssets($assets, TRUE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $expected_1 = ''; + $expected_2 = ''; + $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered external JavaScript with correct defer and random attributes.'); + $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered internal JavaScript with correct defer and random attributes.'); + } + + /** + * Integration test for CSS/JS aggregation. + */ + function testAggregation() { + $build['#attached']['library'][] = 'core/drupal.timezone'; + $build['#attached']['library'][] = 'core/drupal.vertical-tabs'; + $assets = AttachedAssets::createFromRenderArray($build); + + $this->assertEqual(1, count($this->assetResolver->getCssAssets($assets, TRUE)), 'There is a sole aggregated CSS asset.'); + + list($header_js, $footer_js) = $this->assetResolver->getJsAssets($assets, TRUE); + $this->assertEqual([], \Drupal::service('asset.js.collection_renderer')->render($header_js), 'There are 0 JavaScript assets in the header.'); + $rendered_footer_js = \Drupal::service('asset.js.collection_renderer')->render($footer_js); + $this->assertEqual(2, count($rendered_footer_js), 'There are 2 JavaScript assets in the footer.'); + $this->assertEqual('drupal-settings-json', $rendered_footer_js[0]['#attributes']['data-drupal-selector'], 'The first of the two JavaScript assets in the footer has drupal settings.'); + $this->assertEqual(0, strpos($rendered_footer_js[1]['#attributes']['src'], base_path()), 'The second of the two JavaScript assets in the footer has the sole aggregated JavaScript asset.'); + } + + /** + * Tests JavaScript settings. + */ + function testSettings() { + $build = array(); + $build['#attached']['library'][] = 'core/drupalSettings'; + // Nonsensical value to verify if it's possible to override path settings. + $build['#attached']['drupalSettings']['path']['pathPrefix'] = 'yarhar'; + $assets = AttachedAssets::createFromRenderArray($build); + + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + // Cast to string since this returns a \Drupal\Core\Render\Markup object. + $rendered_js = (string) $this->renderer->renderPlain($js_render_array); + + // Parse the generated drupalSettings '), FALSE, 'The JS asset in common_test/js-header appears in the header.'); + $this->assertNotIdentical(strpos($rendered_js, '' . "\n"; + $expected_2 = "\n" . '' . "\n"; + + $this->assertNotIdentical(strpos($rendered_js, $expected_1), FALSE, 'Rendered JavaScript within downlevel-hidden conditional comments.'); + $this->assertNotIdentical(strpos($rendered_js, $expected_2), FALSE, 'Rendered JavaScript within downlevel-revealed conditional comments.'); + } + + /** + * Tests JavaScript versioning. + */ + function testVersionQueryString() { + $build['#attached']['library'][] = 'core/backbone'; + $build['#attached']['library'][] = 'core/domready'; + $assets = AttachedAssets::createFromRenderArray($build); + + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + + $rendered_js = $this->renderer->renderPlain($js_render_array); + $this->assertTrue(strpos($rendered_js, 'core/assets/vendor/backbone/backbone-min.js?v=1.2.3') > 0 && strpos($rendered_js, 'core/assets/vendor/domready/ready.min.js?v=1.0.8') > 0, 'JavaScript version identifiers correctly appended to URLs'); + } + + /** + * Tests JavaScript and CSS asset ordering. + */ + function testRenderOrder() { + $build['#attached']['library'][] = 'common_test/order'; + $assets = AttachedAssets::createFromRenderArray($build); + + // Construct the expected result from the regex. + $expected_order_js = [ + "-8_1", + "-8_2", + "-8_3", + "-8_4", + "-5_1", // The external script. + "-3_1", + "-3_2", + "0_1", + "0_2", + "0_3", + ]; + + // Retrieve the rendered JavaScript and test against the regex. + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $matches = array(); + if (preg_match_all('/weight_([-0-9]+_[0-9]+)/', $rendered_js, $matches)) { + $result = $matches[1]; + } + else { + $result = array(); + } + $this->assertIdentical($result, $expected_order_js, 'JavaScript is added in the expected weight order.'); + + // Construct the expected result from the regex. + $expected_order_css = [ + // Base. + 'base_weight_-101_1', + 'base_weight_-8_1', + 'layout_weight_-101_1', + 'base_weight_0_1', + 'base_weight_0_2', + // Layout. + 'layout_weight_-8_1', + 'component_weight_-101_1', + 'layout_weight_0_1', + 'layout_weight_0_2', + // Component. + 'component_weight_-8_1', + 'state_weight_-101_1', + 'component_weight_0_1', + 'component_weight_0_2', + // State. + 'state_weight_-8_1', + 'theme_weight_-101_1', + 'state_weight_0_1', + 'state_weight_0_2', + // Theme. + 'theme_weight_-8_1', + 'theme_weight_0_1', + 'theme_weight_0_2', + ]; + + // Retrieve the rendered CSS and test against the regex. + $css = $this->assetResolver->getCssAssets($assets, FALSE); + $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); + $rendered_css = $this->renderer->renderPlain($css_render_array); + $matches = array(); + if (preg_match_all('/([a-z]+)_weight_([-0-9]+_[0-9]+)/', $rendered_css, $matches)) { + $result = $matches[0]; + } + else { + $result = array(); + } + $this->assertIdentical($result, $expected_order_css, 'CSS is added in the expected weight order.'); + } + + /** + * Tests rendering the JavaScript with a file's weight above jQuery's. + */ + function testRenderDifferentWeight() { + // If a library contains assets A and B, and A is listed first, then B can + // still make itself appear first by defining a lower weight. + $build['#attached']['library'][] = 'core/jquery'; + $build['#attached']['library'][] = 'common_test/weight'; + $assets = AttachedAssets::createFromRenderArray($build); + + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $this->assertTrue(strpos($rendered_js, 'lighter.css') < strpos($rendered_js, 'first.js'), 'Lighter CSS assets are rendered first.'); + $this->assertTrue(strpos($rendered_js, 'lighter.js') < strpos($rendered_js, 'first.js'), 'Lighter JavaScript assets are rendered first.'); + $this->assertTrue(strpos($rendered_js, 'before-jquery.js') < strpos($rendered_js, 'core/assets/vendor/jquery/jquery.min.js'), 'Rendering a JavaScript file above jQuery.'); + } + + /** + * Tests altering a JavaScript's weight via hook_js_alter(). + * + * @see simpletest_js_alter() + */ + function testAlter() { + // Add both tableselect.js and simpletest.js. + $build['#attached']['library'][] = 'core/drupal.tableselect'; + $build['#attached']['library'][] = 'simpletest/drupal.simpletest'; + $assets = AttachedAssets::createFromRenderArray($build); + + // Render the JavaScript, testing if simpletest.js was altered to be before + // tableselect.js. See simpletest_js_alter() to see where this alteration + // takes place. + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $this->assertTrue(strpos($rendered_js, 'simpletest.js') < strpos($rendered_js, 'core/misc/tableselect.js'), 'Altering JavaScript weight through the alter hook.'); + } + + /** + * Adds a JavaScript library to the page and alters it. + * + * @see common_test_library_info_alter() + */ + function testLibraryAlter() { + // Verify that common_test altered the title of Farbtastic. + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); + $library = $library_discovery->getLibraryByName('core', 'jquery.farbtastic'); + $this->assertEqual($library['version'], '0.0', 'Registered libraries were altered.'); + + // common_test_library_info_alter() also added a dependency on jQuery Form. + $build['#attached']['library'][] = 'core/jquery.farbtastic'; + $assets = AttachedAssets::createFromRenderArray($build); + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $this->assertTrue(strpos($rendered_js, 'core/assets/vendor/jquery-form/jquery.form.min.js'), 'Altered library dependencies are added to the page.'); + } + + /** + * Dynamically defines an asset library and alters it. + */ + function testDynamicLibrary() { + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); + // Retrieve a dynamic library definition. + // @see common_test_library_info_build() + \Drupal::state()->set('common_test.library_info_build_test', TRUE); + $library_discovery->clearCachedDefinitions(); + $dynamic_library = $library_discovery->getLibraryByName('common_test', 'dynamic_library'); + $this->assertTrue(is_array($dynamic_library)); + if ($this->assertTrue(isset($dynamic_library['version']))) { + $this->assertIdentical('1.0', $dynamic_library['version']); + } + // Make sure the dynamic library definition could be altered. + // @see common_test_library_info_alter() + if ($this->assertTrue(isset($dynamic_library['dependencies']))) { + $this->assertIdentical(['core/jquery'], $dynamic_library['dependencies']); + } + } + + /** + * Tests that multiple modules can implement libraries with the same name. + * + * @see common_test.library.yml + */ + function testLibraryNameConflicts() { + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); + $farbtastic = $library_discovery->getLibraryByName('common_test', 'jquery.farbtastic'); + $this->assertEqual($farbtastic['version'], '0.1', 'Alternative libraries can be added to the page.'); + } + + /** + * Tests JavaScript files that have querystrings attached get added right. + */ + function testAddJsFileWithQueryString() { + $build['#attached']['library'][] = 'common_test/querystring'; + $assets = AttachedAssets::createFromRenderArray($build); + + $css = $this->assetResolver->getCssAssets($assets, FALSE); + $js = $this->assetResolver->getJsAssets($assets, FALSE)[1]; + $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.css?arg1=value1&arg2=value2', $css), 'CSS file with query string is correctly added.'); + $this->assertTrue(array_key_exists('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2', $js), 'JavaScript file with query string is correctly added.'); + + $css_render_array = \Drupal::service('asset.css.collection_renderer')->render($css); + $rendered_css = $this->renderer->renderPlain($css_render_array); + $js_render_array = \Drupal::service('asset.js.collection_renderer')->render($js); + $rendered_js = $this->renderer->renderPlain($js_render_array); + $query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; + $this->assertNotIdentical(strpos($rendered_css, ''), FALSE, 'CSS file with query string gets version query string correctly appended..'); + $this->assertNotIdentical(strpos($rendered_js, ''), FALSE, 'JavaScript file with query string gets version query string correctly appended.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php b/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php new file mode 100644 index 0000000..a77adfc --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Asset/LibraryDiscoveryIntegrationTest.php @@ -0,0 +1,296 @@ +container->get('theme_installer')->install(['test_theme', 'classy']); + $this->libraryDiscovery = $this->container->get('library.discovery'); + } + + /** + * Tests that hook_library_info is invoked and the cache is cleared. + */ + public function testHookLibraryInfoByTheme() { + // Activate test_theme and verify that the library 'kitten' is added using + // hook_library_info_alter(). + $this->activateTheme('test_theme'); + $this->assertTrue($this->libraryDiscovery->getLibraryByName('test_theme', 'kitten')); + + // Now make classy the active theme and assert that library is not added. + $this->activateTheme('classy'); + $this->assertFalse($this->libraryDiscovery->getLibraryByName('test_theme', 'kitten')); + } + + /** + * Tests that libraries-override are applied to library definitions. + */ + public function testLibrariesOverride() { + // Assert some classy libraries that will be overridden or removed. + $this->activateTheme('classy'); + $this->assertAssetInLibrary('core/themes/classy/css/components/button.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/collapse-processed.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/container-inline.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/details.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/dialog.css', 'classy', 'dialog', 'css'); + + // Confirmatory assert on core library to be removed. + $this->assertTrue($this->libraryDiscovery->getLibraryByName('core', 'drupal.progress'), 'Confirmatory test on "core/drupal.progress"'); + + // Activate test theme that defines libraries overrides. + $this->activateTheme('test_theme'); + + // Assert that entire library was correctly overridden. + $this->assertEqual($this->libraryDiscovery->getLibraryByName('core', 'drupal.collapse'), $this->libraryDiscovery->getLibraryByName('test_theme', 'collapse'), 'Entire library correctly overridden.'); + + // Assert that classy library assets were correctly overridden or removed. + $this->assertNoAssetInLibrary('core/themes/classy/css/components/button.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/collapse-processed.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/container-inline.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/details.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/dialog.css', 'classy', 'dialog', 'css'); + + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme/css/my-button.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme/css/my-collapse-processed.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('themes/my_theme/css/my-container-inline.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('themes/my_theme/css/my-details.css', 'classy', 'base', 'css'); + + // Assert that entire library was correctly removed. + $this->assertFalse($this->libraryDiscovery->getLibraryByName('core', 'drupal.progress'), 'Entire library correctly removed.'); + + // Assert that overridden library asset still retains attributes. + $library = $this->libraryDiscovery->getLibraryByName('core', 'jquery'); + foreach ($library['js'] as $definition) { + if ($definition['data'] == 'core/modules/system/tests/themes/test_theme/js/collapse.js') { + $this->assertTrue($definition['minified'] && $definition['weight'] == -20, 'Previous attributes retained'); + break; + } + } + } + + /** + * Tests libraries-override on drupalSettings. + */ + public function testLibrariesOverrideDrupalSettings() { + // Activate test theme that attempts to override drupalSettings. + $this->activateTheme('test_theme_libraries_override_with_drupal_settings'); + + // Assert that drupalSettings cannot be overridden and throws an exception. + try { + $this->libraryDiscovery->getLibraryByName('core', 'drupal.ajax'); + $this->fail('Throw Exception when trying to override drupalSettings'); + } + catch (InvalidLibrariesOverrideSpecificationException $e) { + $expected_message = 'drupalSettings may not be overridden in libraries-override. Trying to override core/drupal.ajax/drupalSettings. Use hook_library_info_alter() instead.'; + $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when trying to override drupalSettings'); + } + } + + /** + * Tests libraries-override on malformed assets. + */ + public function testLibrariesOverrideMalformedAsset() { + // Activate test theme that overrides with a malformed asset. + $this->activateTheme('test_theme_libraries_override_with_invalid_asset'); + + // Assert that improperly formed asset "specs" throw an exception. + try { + $this->libraryDiscovery->getLibraryByName('core', 'drupal.dialog'); + $this->fail('Throw Exception when specifying invalid override'); + } + catch (InvalidLibrariesOverrideSpecificationException $e) { + $expected_message = 'Library asset core/drupal.dialog/css is not correctly specified. It should be in the form "extension/library_name/sub_key/path/to/asset.js".'; + $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when specifying invalid override'); + } + } + + /** + * Tests library assets with other ways for specifying paths. + */ + public function testLibrariesOverrideOtherAssetLibraryNames() { + // Activate a test theme that defines libraries overrides on other types of + // assets. + $this->activateTheme('test_theme'); + + // Assert Drupal-relative paths. + $this->assertAssetInLibrary('themes/my_theme/css/dropbutton.css', 'core', 'drupal.dropbutton', 'css'); + + // Assert stream wrapper paths. + $this->assertAssetInLibrary('public://my_css/vertical-tabs.css', 'core', 'drupal.vertical-tabs', 'css'); + + // Assert a protocol-relative URI. + $this->assertAssetInLibrary('//my-server/my_theme/css/jquery_ui.css', 'core', 'jquery.ui', 'css'); + + // Assert an absolute URI. + $this->assertAssetInLibrary('http://example.com/my_theme/css/farbtastic.css', 'core', 'jquery.farbtastic', 'css'); + } + + /** + * Tests that base theme libraries-override still apply in sub themes. + */ + public function testBaseThemeLibrariesOverrideInSubTheme() { + // Activate a test theme that has subthemes. + $this->activateTheme('test_subtheme'); + + // Assert that libraries-override specified in the base theme still applies + // in the sub theme. + $this->assertNoAssetInLibrary('core/misc/dialog/dialog.js', 'core', 'drupal.dialog', 'js'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_basetheme/css/farbtastic.css', 'core', 'jquery.farbtastic', 'css'); + } + + /** + * Tests libraries-extend. + */ + public function testLibrariesExtend() { + // Activate classy themes and verify the libraries are not extended. + $this->activateTheme('classy'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_1.css', 'classy', 'book-navigation', 'css'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/js/extend_1.js', 'classy', 'book-navigation', 'js'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_2.css', 'classy', 'book-navigation', 'css'); + + // Activate the theme that extends the book-navigation library in classy. + $this->activateTheme('test_theme_libraries_extend'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_1.css', 'classy', 'book-navigation', 'css'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/js/extend_1.js', 'classy', 'book-navigation', 'js'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_2.css', 'classy', 'book-navigation', 'css'); + + // Activate a sub theme and confirm that it inherits the library assets + // extended in the base theme as well as its own. + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_basetheme/css/base-libraries-extend.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_subtheme/css/sub-libraries-extend.css', 'classy', 'base', 'css'); + $this->activateTheme('test_subtheme'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_basetheme/css/base-libraries-extend.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_subtheme/css/sub-libraries-extend.css', 'classy', 'base', 'css'); + + // Activate test theme that extends with a non-existent library. An + // exception should be thrown. + $this->activateTheme('test_theme_libraries_extend'); + try { + $this->libraryDiscovery->getLibraryByName('core', 'drupal.dialog'); + $this->fail('Throw Exception when specifying non-existent libraries-extend.'); + } + catch (InvalidLibrariesExtendSpecificationException $e) { + $expected_message = 'The specified library "test_theme_libraries_extend/non_existent_library" does not exist.'; + $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when specifying non-existent libraries-extend.'); + } + + // Also, test non-string libraries-extend. An exception should be thrown. + $this->container->get('theme_installer')->install(['test_theme']); + try { + $this->libraryDiscovery->getLibraryByName('test_theme', 'collapse'); + $this->fail('Throw Exception when specifying non-string libraries-extend.'); + } + catch (InvalidLibrariesExtendSpecificationException $e) { + $expected_message = 'The libraries-extend specification for each library must be a list of strings.'; + $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when specifying non-string libraries-extend.'); + } + } + + /** + * Activates a specified theme. + * + * Installs the theme if not already installed and makes it the active theme. + * + * @param string $theme_name + * The name of the theme to be activated. + */ + protected function activateTheme($theme_name) { + $this->container->get('theme_installer')->install([$theme_name]); + + /** @var \Drupal\Core\Theme\ThemeInitializationInterface $theme_initializer */ + $theme_initializer = $this->container->get('theme.initialization'); + + /** @var \Drupal\Core\Theme\ThemeManagerInterface $theme_manager */ + $theme_manager = $this->container->get('theme.manager'); + + $theme_manager->setActiveTheme($theme_initializer->getActiveThemeByName($theme_name)); + + $this->libraryDiscovery->clearCachedDefinitions(); + + // Assert message. + $this->pass(sprintf('Activated theme "%s"', $theme_name)); + } + + /** + * Asserts that the specified asset is in the given library. + * + * @param string $asset + * The asset file with the path for the file. + * @param string $extension + * The extension in which the $library is defined. + * @param string $library_name + * Name of the library. + * @param mixed $sub_key + * The library sub key where the given asset is defined. + * @param string $message + * (optional) A message to display with the assertion. + * + * @return bool + * TRUE if the specified asset is found in the library. + */ + protected function assertAssetInLibrary($asset, $extension, $library_name, $sub_key, $message = NULL) { + if (!isset($message)) { + $message = sprintf('Asset %s found in library "%s/%s"', $asset, $extension, $library_name); + } + $library = $this->libraryDiscovery->getLibraryByName($extension, $library_name); + foreach ($library[$sub_key] as $definition) { + if ($asset == $definition['data']) { + return $this->pass($message); + } + } + return $this->fail($message); + } + + /** + * Asserts that the specified asset is not in the given library. + * + * @param string $asset + * The asset file with the path for the file. + * @param string $extension + * The extension in which the $library_name is defined. + * @param string $library_name + * Name of the library. + * @param mixed $sub_key + * The library sub key where the given asset is defined. + * @param string $message + * (optional) A message to display with the assertion. + * + * @return bool + * TRUE if the specified asset is not found in the library. + */ + protected function assertNoAssetInLibrary($asset, $extension, $library_name, $sub_key, $message = NULL) { + if (!isset($message)) { + $message = sprintf('Asset %s not found in library "%s/%s"', $asset, $extension, $library_name); + } + $library = $this->libraryDiscovery->getLibraryByName($extension, $library_name); + foreach ($library[$sub_key] as $definition) { + if ($asset == $definition['data']) { + return $this->fail($message); + } + } + return $this->pass($message); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php new file mode 100644 index 0000000..c3d3e30 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php @@ -0,0 +1,201 @@ +allThemes); + $this->container->get('theme_installer')->install($this->allThemes); + + // Enable all core modules. + $all_modules = system_rebuild_module_data(); + $all_modules = array_filter($all_modules, function ($module) { + // Filter contrib, hidden, already enabled modules and modules in the + // Testing package. + if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') { + return FALSE; + } + return TRUE; + }); + $this->allModules = array_keys($all_modules); + $this->allModules[] = 'system'; + sort($this->allModules); + $this->container->get('module_installer')->install($this->allModules); + + $this->themeHandler = $this->container->get('theme_handler'); + $this->themeInitialization = $this->container->get('theme.initialization'); + $this->themeManager = $this->container->get('theme.manager'); + $this->libraryDiscovery = $this->container->get('library.discovery'); + } + + /** + * Ensures that all core module and theme library files exist. + */ + public function testCoreLibraryCompleteness() { + // First verify all libraries with no active theme. + $this->verifyLibraryFilesExist($this->getAllLibraries()); + + // Then verify all libraries for each core theme. This may seem like + // overkill but themes can override and extend other extensions' libraries + // and these changes are only applied for the active theme. + foreach ($this->allThemes as $theme) { + $this->themeManager->setActiveTheme($this->themeInitialization->getActiveThemeByName($theme)); + $this->libraryDiscovery->clearCachedDefinitions(); + + $this->verifyLibraryFilesExist($this->getAllLibraries()); + } + } + + /** + * Checks that all the library files exist. + * + * @param array[] + * An array of library definitions, keyed by extension, then by library, and + * so on. + */ + protected function verifyLibraryFilesExist($library_definitions) { + $root = \Drupal::root(); + foreach ($library_definitions as $extension => $libraries) { + foreach ($libraries as $library_name => $library) { + if (in_array("$extension/$library_name", $this->librariesToSkip)) { + continue; + } + + // Check that all the assets exist. + foreach (['css', 'js'] as $asset_type) { + foreach ($library[$asset_type] as $asset) { + $file = $asset['data']; + $path = $root . '/' . $file; + // Only check and assert each file path once. + if (!isset($this->pathsChecked[$path])) { + $this->assertTrue(is_file($path), "$file file referenced from the $extension/$library_name library exists."); + $this->pathsChecked[$path] = TRUE; + } + } + } + } + } + } + + /** + * Gets all libraries for core and all installed modules. + * + * @return \Drupal\Core\Extension\Extension[] + */ + protected function getAllLibraries() { + $modules = \Drupal::moduleHandler()->getModuleList(); + $extensions = $modules; + $module_list = array_keys($modules); + sort($module_list); + $this->assertEqual($this->allModules, $module_list, 'All core modules are installed.'); + + $themes = $this->themeHandler->listInfo(); + $extensions += $themes; + $theme_list = array_keys($themes); + sort($theme_list); + $this->assertEqual($this->allThemes, $theme_list, 'All core themes are installed.'); + + $libraries['core'] = $this->libraryDiscovery->getLibrariesByExtension('core'); + + $root = \Drupal::root(); + foreach ($extensions as $extension_name => $extension) { + $library_file = $extension->getPath() . '/' . $extension_name . '.libraries.yml'; + if (is_file($root . '/' . $library_file)) { + $libraries[$extension_name] = $this->libraryDiscovery->getLibrariesByExtension($extension_name); + } + } + return $libraries; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php new file mode 100644 index 0000000..6cdc4b3 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Bootstrap/GetFilenameTest.php @@ -0,0 +1,66 @@ +assertIdentical(drupal_get_filename('module', 'system'), 'core/modules/system/system.info.yml'); + + // Retrieving the location of a theme. + \Drupal::service('theme_handler')->install(array('stark')); + $this->assertIdentical(drupal_get_filename('theme', 'stark'), 'core/themes/stark/stark.info.yml'); + + // Retrieving the location of a theme engine. + $this->assertIdentical(drupal_get_filename('theme_engine', 'twig'), 'core/themes/engines/twig/twig.info.yml'); + + // Retrieving the location of a profile. Profiles are a special case with + // a fixed location and naming. + $this->assertIdentical(drupal_get_filename('profile', 'testing'), 'core/profiles/testing/testing.info.yml'); + + + // Generate a non-existing module name. + $non_existing_module = uniqid("", TRUE); + + // Set a custom error handler so we can ignore the file not found error. + set_error_handler(function($severity, $message, $file, $line) { + // Skip error handling if this is a "file not found" error. + if (strstr($message, 'is missing from the file system:')) { + \Drupal::state()->set('get_filename_test_triggered_error', TRUE); + return; + } + throw new \ErrorException($message, 0, $severity, $file, $line); + }); + $this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for an item that does not exist returns NULL.'); + $this->assertTrue(\Drupal::state()->get('get_filename_test_triggered_error'), 'Searching for an item that does not exist triggers an error.'); + // Restore the original error handler. + restore_error_handler(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Bootstrap/ResettableStaticTest.php b/core/tests/Drupal/KernelTests/Core/Bootstrap/ResettableStaticTest.php new file mode 100644 index 0000000..7b90ec1 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Bootstrap/ResettableStaticTest.php @@ -0,0 +1,41 @@ +assertEqual($var, 'foo', 'Variable returned by drupal_static() was set to its default.'); + + // Call the specific reset and the global reset each twice to ensure that + // multiple resets can be issued without odd side effects. + $var = 'bar'; + drupal_static_reset($name); + $this->assertEqual($var, 'foo', 'Variable was reset after first invocation of name-specific reset.'); + $var = 'bar'; + drupal_static_reset($name); + $this->assertEqual($var, 'foo', 'Variable was reset after second invocation of name-specific reset.'); + $var = 'bar'; + drupal_static_reset(); + $this->assertEqual($var, 'foo', 'Variable was reset after first invocation of global reset.'); + $var = 'bar'; + drupal_static_reset(); + $this->assertEqual($var, 'foo', 'Variable was reset after second invocation of global reset.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/ApcuBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/ApcuBackendTest.php new file mode 100644 index 0000000..0161d2c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/ApcuBackendTest.php @@ -0,0 +1,207 @@ +getRequirements(); + if (!empty($requirements)) { + foreach ($requirements as $message) { + $this->pass($message); + } + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + protected function createCacheBackend($bin) { + if (version_compare(phpversion('apcu'), '5.0.0', '>=')) { + return new ApcuBackend($bin, $this->databasePrefix, \Drupal::service('cache_tags.invalidator.checksum')); + } + else { + return new Apcu4Backend($bin, $this->databasePrefix, \Drupal::service('cache_tags.invalidator.checksum')); + } + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + foreach ($this->cachebackends as $bin => $cachebackend) { + $this->cachebackends[$bin]->removeBin(); + } + parent::tearDown(); + } + + /** + * {@inheritdoc} + */ + public function testSetGet() { + if ($this->requirementsFail()) { + return; + } + parent::testSetGet(); + + // Make sure entries are permanent (i.e. no TTL). + $backend = $this->getCacheBackend($this->getTestBin()); + $key = $backend->getApcuKey('TEST8'); + + if (class_exists('\APCUIterator')) { + $iterator = new \APCUIterator('/^' . $key . '/'); + } + else { + $iterator = new \APCIterator('user', '/^' . $key . '/'); + } + + foreach ($iterator as $item) { + $this->assertEqual(0, $item['ttl']); + $found = TRUE; + } + $this->assertTrue($found); + } + + /** + * {@inheritdoc} + */ + public function testDelete() { + if ($this->requirementsFail()) { + return; + } + parent::testDelete(); + } + + /** + * {@inheritdoc} + */ + public function testValueTypeIsKept() { + if ($this->requirementsFail()) { + return; + } + parent::testValueTypeIsKept(); + } + + /** + * {@inheritdoc} + */ + public function testGetMultiple() { + if ($this->requirementsFail()) { + return; + } + parent::testGetMultiple(); + } + + /** + * {@inheritdoc} + */ + public function testSetMultiple() { + if ($this->requirementsFail()) { + return; + } + parent::testSetMultiple(); + } + + /** + * {@inheritdoc} + */ + public function testDeleteMultiple() { + if ($this->requirementsFail()) { + return; + } + parent::testDeleteMultiple(); + } + + /** + * {@inheritdoc} + */ + public function testDeleteAll() { + if ($this->requirementsFail()) { + return; + } + parent::testDeleteAll(); + } + + /** + * {@inheritdoc} + */ + public function testInvalidate() { + if ($this->requirementsFail()) { + return; + } + parent::testInvalidate(); + } + + /** + * {@inheritdoc} + */ + public function testInvalidateTags() { + if ($this->requirementsFail()) { + return; + } + parent::testInvalidateTags(); + } + + /** + * {@inheritdoc} + */ + public function testInvalidateAll() { + if ($this->requirementsFail()) { + return; + } + parent::testInvalidateAll(); + } + + /** + * {@inheritdoc} + */ + public function testRemoveBin() { + if ($this->requirementsFail()) { + return; + } + parent::testRemoveBin(); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/BackendChainTest.php b/core/tests/Drupal/KernelTests/Core/Cache/BackendChainTest.php new file mode 100644 index 0000000..0282180 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/BackendChainTest.php @@ -0,0 +1,29 @@ +appendBackend(new MemoryBackend('foo')) + ->prependBackend(new MemoryBackend('bar')) + ->appendBackend(new MemoryBackend('baz')); + + \Drupal::service('cache_tags.invalidator')->addInvalidator($chain); + + return $chain; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/CacheContextOptimizationTest.php b/core/tests/Drupal/KernelTests/Core/Cache/CacheContextOptimizationTest.php new file mode 100644 index 0000000..0ae0230 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/CacheContextOptimizationTest.php @@ -0,0 +1,119 @@ +installEntitySchema('user'); + $this->installConfig(['user']); + $this->installSchema('system', ['sequences']); + } + + /** + * Ensures that 'user.permissions' cache context is able to define cache tags. + */ + public function testUserPermissionCacheContextOptimization() { + $user1 = $this->createUser(); + $this->assertEqual($user1->id(), 1); + + $authenticated_user = $this->createUser(['administer permissions']); + $role = $authenticated_user->getRoles()[1]; + + $test_element = [ + '#cache' => [ + 'keys' => ['test'], + 'contexts' => ['user', 'user.permissions'], + ], + ]; + \Drupal::service('account_switcher')->switchTo($authenticated_user); + $element = $test_element; + $element['#markup'] = 'content for authenticated users'; + $output = \Drupal::service('renderer')->renderRoot($element); + $this->assertEqual($output, 'content for authenticated users'); + + // Verify that the render caching is working so that other tests can be + // trusted. + $element = $test_element; + $element['#markup'] = 'this should not be visible'; + $output = \Drupal::service('renderer')->renderRoot($element); + $this->assertEqual($output, 'content for authenticated users'); + + // Even though the cache contexts have been optimized to only include 'user' + // cache context, the element should have been changed because + // 'user.permissions' cache context defined a cache tags for permission + // changes, which should have bubbled up for the element when it was + // optimized away. + Role::load($role) + ->revokePermission('administer permissions') + ->save(); + $element = $test_element; + $element['#markup'] = 'this should be visible'; + $output = \Drupal::service('renderer')->renderRoot($element); + $this->assertEqual($output, 'this should be visible'); + } + + /** + * Ensures that 'user.roles' still works when it is optimized away. + */ + public function testUserRolesCacheContextOptimization() { + $root_user = $this->createUser(); + $this->assertEqual($root_user->id(), 1); + + $authenticated_user = $this->createUser(['administer permissions']); + $role = $authenticated_user->getRoles()[1]; + + $test_element = [ + '#cache' => [ + 'keys' => ['test'], + 'contexts' => ['user', 'user.roles'], + ], + ]; + \Drupal::service('account_switcher')->switchTo($authenticated_user); + $element = $test_element; + $element['#markup'] = 'content for authenticated users'; + $output = \Drupal::service('renderer')->renderRoot($element); + $this->assertEqual($output, 'content for authenticated users'); + + // Verify that the render caching is working so that other tests can be + // trusted. + $element = $test_element; + $element['#markup'] = 'this should not be visible'; + $output = \Drupal::service('renderer')->renderRoot($element); + $this->assertEqual($output, 'content for authenticated users'); + + // Even though the cache contexts have been optimized to only include 'user' + // cache context, the element should have been changed because 'user.roles' + // cache context defined a cache tag for user entity changes, which should + // have bubbled up for the element when it was optimized away. + $authenticated_user->removeRole($role); + $authenticated_user->save(); + $element = $test_element; + $element['#markup'] = 'this should be visible'; + $output = \Drupal::service('renderer')->renderRoot($element); + $this->assertEqual($output, 'this should be visible'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php new file mode 100644 index 0000000..3021b04 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/ChainedFastBackendTest.php @@ -0,0 +1,32 @@ +addInvalidator($backend); + return $backend; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php new file mode 100644 index 0000000..484ee9c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php @@ -0,0 +1,60 @@ +register('cache_factory', 'Drupal\Core\Cache\CacheFactory') + ->addArgument(new Reference('settings')) + ->addMethodCall('setContainer', array(new Reference('service_container'))); + } + + public function testTagInvalidations() { + // Create cache entry in multiple bins. + $tags = array('test_tag:1', 'test_tag:2', 'test_tag:3'); + $bins = array('data', 'bootstrap', 'render'); + foreach ($bins as $bin) { + $bin = \Drupal::cache($bin); + $bin->set('test', 'value', Cache::PERMANENT, $tags); + $this->assertTrue($bin->get('test'), 'Cache item was set in bin.'); + } + + $invalidations_before = intval(db_select('cachetags')->fields('cachetags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField()); + Cache::invalidateTags(array('test_tag:2')); + + // Test that cache entry has been invalidated in multiple bins. + foreach ($bins as $bin) { + $bin = \Drupal::cache($bin); + $this->assertFalse($bin->get('test'), 'Tag invalidation affected item in bin.'); + } + + // Test that only one tag invalidation has occurred. + $invalidations_after = intval(db_select('cachetags')->fields('cachetags', array('invalidations'))->condition('tag', 'test_tag:2')->execute()->fetchField()); + $this->assertEqual($invalidations_after, $invalidations_before + 1, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php new file mode 100644 index 0000000..fd1c942 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php @@ -0,0 +1,51 @@ +container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin); + } + + /** + * {@inheritdoc} + */ + public function testSetGet() { + parent::testSetGet(); + $backend = $this->getCacheBackend(); + + // Set up a cache ID that is not ASCII and longer than 255 characters so we + // can test cache ID normalization. + $cid_long = str_repeat('愛€', 500); + $cached_value_long = $this->randomMachineName(); + $backend->set($cid_long, $cached_value_long); + $this->assertIdentical($cached_value_long, $backend->get($cid_long)->data, "Backend contains the correct value for long, non-ASCII cache id."); + + $cid_short = '愛1€'; + $cached_value_short = $this->randomMachineName(); + $backend->set($cid_short, $cached_value_short); + $this->assertIdentical($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id."); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php b/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php new file mode 100644 index 0000000..d5f3e95 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/GenericCacheBackendUnitTestBase.php @@ -0,0 +1,620 @@ +testBin)) { + $this->testBin = 'page'; + } + return $this->testBin; + } + + /** + * Creates a cache backend to test. + * + * Override this method to test a CacheBackend. + * + * @param string $bin + * Bin name to use for this backend instance. + * + * @return \Drupal\Core\Cache\CacheBackendInterface + * Cache backend to test. + */ + protected abstract function createCacheBackend($bin); + + /** + * Allows specific implementation to change the environment before a test run. + */ + public function setUpCacheBackend() { + } + + /** + * Allows alteration of environment after a test run but before tear down. + * + * Used before the real tear down because the tear down will change things + * such as the database prefix. + */ + public function tearDownCacheBackend() { + } + + /** + * Gets a backend to test; this will get a shared instance set in the object. + * + * @return \Drupal\Core\Cache\CacheBackendInterface + * Cache backend to test. + */ + protected function getCacheBackend($bin = NULL) { + if (!isset($bin)) { + $bin = $this->getTestBin(); + } + if (!isset($this->cachebackends[$bin])) { + $this->cachebackends[$bin] = $this->createCacheBackend($bin); + // Ensure the backend is empty. + $this->cachebackends[$bin]->deleteAll(); + } + return $this->cachebackends[$bin]; + } + + protected function setUp() { + $this->cachebackends = array(); + $this->defaultValue = $this->randomMachineName(10); + + parent::setUp(); + + $this->setUpCacheBackend(); + } + + protected function tearDown() { + // Destruct the registered backend, each test will get a fresh instance, + // properly emptying it here ensure that on persistent data backends they + // will come up empty the next test. + foreach ($this->cachebackends as $bin => $cachebackend) { + $this->cachebackends[$bin]->deleteAll(); + } + unset($this->cachebackends); + + $this->tearDownCacheBackend(); + + parent::tearDown(); + } + + /** + * Tests the get and set methods of Drupal\Core\Cache\CacheBackendInterface. + */ + public function testSetGet() { + $backend = $this->getCacheBackend(); + + $this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1."); + $with_backslash = array('foo' => '\Drupal\foo\Bar'); + $backend->set('test1', $with_backslash); + $cached = $backend->get('test1'); + $this->assert(is_object($cached), "Backend returned an object for cache id test1."); + $this->assertIdentical($with_backslash, $cached->data); + $this->assertTrue($cached->valid, 'Item is marked as valid.'); + // We need to round because microtime may be rounded up in the backend. + $this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.'); + + $this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2."); + $backend->set('test2', array('value' => 3), REQUEST_TIME + 3); + $cached = $backend->get('test2'); + $this->assert(is_object($cached), "Backend returned an object for cache id test2."); + $this->assertIdentical(array('value' => 3), $cached->data); + $this->assertTrue($cached->valid, 'Item is marked as valid.'); + $this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($cached->expire, REQUEST_TIME + 3, 'Expire time is correct.'); + + $backend->set('test3', 'foobar', REQUEST_TIME - 3); + $this->assertFalse($backend->get('test3'), 'Invalid item not returned.'); + $cached = $backend->get('test3', TRUE); + $this->assert(is_object($cached), 'Backend returned an object for cache id test3.'); + $this->assertFalse($cached->valid, 'Item is marked as valid.'); + $this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($cached->expire, REQUEST_TIME - 3, 'Expire time is correct.'); + + $this->assertIdentical(FALSE, $backend->get('test4'), "Backend does not contain data for cache id test4."); + $with_eof = array('foo' => "\nEOF\ndata"); + $backend->set('test4', $with_eof); + $cached = $backend->get('test4'); + $this->assert(is_object($cached), "Backend returned an object for cache id test4."); + $this->assertIdentical($with_eof, $cached->data); + $this->assertTrue($cached->valid, 'Item is marked as valid.'); + $this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.'); + + $this->assertIdentical(FALSE, $backend->get('test5'), "Backend does not contain data for cache id test5."); + $with_eof_and_semicolon = array('foo' => "\nEOF;\ndata"); + $backend->set('test5', $with_eof_and_semicolon); + $cached = $backend->get('test5'); + $this->assert(is_object($cached), "Backend returned an object for cache id test5."); + $this->assertIdentical($with_eof_and_semicolon, $cached->data); + $this->assertTrue($cached->valid, 'Item is marked as valid.'); + $this->assertTrue($cached->created >= REQUEST_TIME && $cached->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($cached->expire, Cache::PERMANENT, 'Expire time is correct.'); + + $with_variable = array('foo' => '$bar'); + $backend->set('test6', $with_variable); + $cached = $backend->get('test6'); + $this->assert(is_object($cached), "Backend returned an object for cache id test6."); + $this->assertIdentical($with_variable, $cached->data); + + // Make sure that a cached object is not affected by changing the original. + $data = new \stdClass(); + $data->value = 1; + $data->obj = new \stdClass(); + $data->obj->value = 2; + $backend->set('test7', $data); + $expected_data = clone $data; + // Add a property to the original. It should not appear in the cached data. + $data->this_should_not_be_in_the_cache = TRUE; + $cached = $backend->get('test7'); + $this->assert(is_object($cached), "Backend returned an object for cache id test7."); + $this->assertEqual($expected_data, $cached->data); + $this->assertFalse(isset($cached->data->this_should_not_be_in_the_cache)); + // Add a property to the cache data. It should not appear when we fetch + // the data from cache again. + $cached->data->this_should_not_be_in_the_cache = TRUE; + $fresh_cached = $backend->get('test7'); + $this->assertFalse(isset($fresh_cached->data->this_should_not_be_in_the_cache)); + + // Check with a long key. + $cid = str_repeat('a', 300); + $backend->set($cid, 'test'); + $this->assertEqual('test', $backend->get($cid)->data); + + // Check that the cache key is case sensitive. + $backend->set('TEST8', 'value'); + $this->assertEqual('value', $backend->get('TEST8')->data); + $this->assertFalse($backend->get('test8')); + + // Calling ::set() with invalid cache tags. This should fail an assertion. + try { + $backend->set('assertion_test', 'value', Cache::PERMANENT, ['node' => [3, 5, 7]]); + $this->fail('::set() was called with invalid cache tags, runtime assertion did not fail.'); + } + catch (\AssertionError $e) { + $this->pass('::set() was called with invalid cache tags, runtime assertion failed.'); + } + } + + /** + * Tests Drupal\Core\Cache\CacheBackendInterface::delete(). + */ + public function testDelete() { + $backend = $this->getCacheBackend(); + + $this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1."); + $backend->set('test1', 7); + $this->assert(is_object($backend->get('test1')), "Backend returned an object for cache id test1."); + + $this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2."); + $backend->set('test2', 3); + $this->assert(is_object($backend->get('test2')), "Backend returned an object for cache id %cid."); + + $backend->delete('test1'); + $this->assertIdentical(FALSE, $backend->get('test1'), "Backend does not contain data for cache id test1 after deletion."); + + $this->assert(is_object($backend->get('test2')), "Backend still has an object for cache id test2."); + + $backend->delete('test2'); + $this->assertIdentical(FALSE, $backend->get('test2'), "Backend does not contain data for cache id test2 after deletion."); + + $long_cid = str_repeat('a', 300); + $backend->set($long_cid, 'test'); + $backend->delete($long_cid); + $this->assertIdentical(FALSE, $backend->get($long_cid), "Backend does not contain data for long cache id after deletion."); + } + + /** + * Tests data type preservation. + */ + public function testValueTypeIsKept() { + $backend = $this->getCacheBackend(); + + $variables = array( + 'test1' => 1, + 'test2' => '0', + 'test3' => '', + 'test4' => 12.64, + 'test5' => FALSE, + 'test6' => array(1, 2, 3), + ); + + // Create cache entries. + foreach ($variables as $cid => $data) { + $backend->set($cid, $data); + } + + // Retrieve and test cache objects. + foreach ($variables as $cid => $value) { + $object = $backend->get($cid); + $this->assert(is_object($object), sprintf("Backend returned an object for cache id %s.", $cid)); + $this->assertIdentical($value, $object->data, sprintf("Data of cached id %s kept is identical in type and value", $cid)); + } + } + + /** + * Tests Drupal\Core\Cache\CacheBackendInterface::getMultiple(). + */ + public function testGetMultiple() { + $backend = $this->getCacheBackend(); + + // Set numerous testing keys. + $long_cid = str_repeat('a', 300); + $backend->set('test1', 1); + $backend->set('test2', 3); + $backend->set('test3', 5); + $backend->set('test4', 7); + $backend->set('test5', 11); + $backend->set('test6', 13); + $backend->set('test7', 17); + $backend->set($long_cid, 300); + + // Mismatch order for harder testing. + $reference = array( + 'test3', + 'test7', + 'test21', // Cid does not exist. + 'test6', + 'test19', // Cid does not exist until added before second getMultiple(). + 'test2', + ); + + $cids = $reference; + $ret = $backend->getMultiple($cids); + // Test return - ensure it contains existing cache ids. + $this->assert(isset($ret['test2']), "Existing cache id test2 is set."); + $this->assert(isset($ret['test3']), "Existing cache id test3 is set."); + $this->assert(isset($ret['test6']), "Existing cache id test6 is set."); + $this->assert(isset($ret['test7']), "Existing cache id test7 is set."); + // Test return - ensure that objects has expected properties. + $this->assertTrue($ret['test2']->valid, 'Item is marked as valid.'); + $this->assertTrue($ret['test2']->created >= REQUEST_TIME && $ret['test2']->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($ret['test2']->expire, Cache::PERMANENT, 'Expire time is correct.'); + // Test return - ensure it does not contain nonexistent cache ids. + $this->assertFalse(isset($ret['test19']), "Nonexistent cache id test19 is not set."); + $this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set."); + // Test values. + $this->assertIdentical($ret['test2']->data, 3, "Existing cache id test2 has the correct value."); + $this->assertIdentical($ret['test3']->data, 5, "Existing cache id test3 has the correct value."); + $this->assertIdentical($ret['test6']->data, 13, "Existing cache id test6 has the correct value."); + $this->assertIdentical($ret['test7']->data, 17, "Existing cache id test7 has the correct value."); + // Test $cids array - ensure it contains cache id's that do not exist. + $this->assert(in_array('test19', $cids), "Nonexistent cache id test19 is in cids array."); + $this->assert(in_array('test21', $cids), "Nonexistent cache id test21 is in cids array."); + // Test $cids array - ensure it does not contain cache id's that exist. + $this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array."); + $this->assertFalse(in_array('test3', $cids), "Existing cache id test3 is not in cids array."); + $this->assertFalse(in_array('test6', $cids), "Existing cache id test6 is not in cids array."); + $this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array."); + + // Test a second time after deleting and setting new keys which ensures that + // if the backend uses statics it does not cause unexpected results. + $backend->delete('test3'); + $backend->delete('test6'); + $backend->set('test19', 57); + + $cids = $reference; + $ret = $backend->getMultiple($cids); + // Test return - ensure it contains existing cache ids. + $this->assert(isset($ret['test2']), "Existing cache id test2 is set"); + $this->assert(isset($ret['test7']), "Existing cache id test7 is set"); + $this->assert(isset($ret['test19']), "Added cache id test19 is set"); + // Test return - ensure it does not contain nonexistent cache ids. + $this->assertFalse(isset($ret['test3']), "Deleted cache id test3 is not set"); + $this->assertFalse(isset($ret['test6']), "Deleted cache id test6 is not set"); + $this->assertFalse(isset($ret['test21']), "Nonexistent cache id test21 is not set"); + // Test values. + $this->assertIdentical($ret['test2']->data, 3, "Existing cache id test2 has the correct value."); + $this->assertIdentical($ret['test7']->data, 17, "Existing cache id test7 has the correct value."); + $this->assertIdentical($ret['test19']->data, 57, "Added cache id test19 has the correct value."); + // Test $cids array - ensure it contains cache id's that do not exist. + $this->assert(in_array('test3', $cids), "Deleted cache id test3 is in cids array."); + $this->assert(in_array('test6', $cids), "Deleted cache id test6 is in cids array."); + $this->assert(in_array('test21', $cids), "Nonexistent cache id test21 is in cids array."); + // Test $cids array - ensure it does not contain cache id's that exist. + $this->assertFalse(in_array('test2', $cids), "Existing cache id test2 is not in cids array."); + $this->assertFalse(in_array('test7', $cids), "Existing cache id test7 is not in cids array."); + $this->assertFalse(in_array('test19', $cids), "Added cache id test19 is not in cids array."); + + // Test with a long $cid and non-numeric array key. + $cids = array('key:key' => $long_cid); + $return = $backend->getMultiple($cids); + $this->assertEqual(300, $return[$long_cid]->data); + $this->assertTrue(empty($cids)); + } + + /** + * Tests \Drupal\Core\Cache\CacheBackendInterface::setMultiple(). + */ + public function testSetMultiple() { + $backend = $this->getCacheBackend(); + + $future_expiration = REQUEST_TIME + 100; + + // Set multiple testing keys. + $backend->set('cid_1', 'Some other value'); + $items = array( + 'cid_1' => array('data' => 1), + 'cid_2' => array('data' => 2), + 'cid_3' => array('data' => array(1, 2)), + 'cid_4' => array('data' => 1, 'expire' => $future_expiration), + 'cid_5' => array('data' => 1, 'tags' => array('test:a', 'test:b')), + ); + $backend->setMultiple($items); + $cids = array_keys($items); + $cached = $backend->getMultiple($cids); + + $this->assertEqual($cached['cid_1']->data, $items['cid_1']['data'], 'Over-written cache item set correctly.'); + $this->assertTrue($cached['cid_1']->valid, 'Item is marked as valid.'); + $this->assertTrue($cached['cid_1']->created >= REQUEST_TIME && $cached['cid_1']->created <= round(microtime(TRUE), 3), 'Created time is correct.'); + $this->assertEqual($cached['cid_1']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.'); + + $this->assertEqual($cached['cid_2']->data, $items['cid_2']['data'], 'New cache item set correctly.'); + $this->assertEqual($cached['cid_2']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.'); + + $this->assertEqual($cached['cid_3']->data, $items['cid_3']['data'], 'New cache item with serialized data set correctly.'); + $this->assertEqual($cached['cid_3']->expire, CacheBackendInterface::CACHE_PERMANENT, 'Cache expiration defaults to permanent.'); + + $this->assertEqual($cached['cid_4']->data, $items['cid_4']['data'], 'New cache item set correctly.'); + $this->assertEqual($cached['cid_4']->expire, $future_expiration, 'Cache expiration has been correctly set.'); + + $this->assertEqual($cached['cid_5']->data, $items['cid_5']['data'], 'New cache item set correctly.'); + + // Calling ::setMultiple() with invalid cache tags. This should fail an + // assertion. + try { + $items = [ + 'exception_test_1' => array('data' => 1, 'tags' => []), + 'exception_test_2' => array('data' => 2, 'tags' => ['valid']), + 'exception_test_3' => array('data' => 3, 'tags' => ['node' => [3, 5, 7]]), + ]; + $backend->setMultiple($items); + $this->fail('::setMultiple() was called with invalid cache tags, runtime assertion did not fail.'); + } + catch (\AssertionError $e) { + $this->pass('::setMultiple() was called with invalid cache tags, runtime assertion failed.'); + } + } + + /** + * Test Drupal\Core\Cache\CacheBackendInterface::delete() and + * Drupal\Core\Cache\CacheBackendInterface::deleteMultiple(). + */ + public function testDeleteMultiple() { + $backend = $this->getCacheBackend(); + + // Set numerous testing keys. + $backend->set('test1', 1); + $backend->set('test2', 3); + $backend->set('test3', 5); + $backend->set('test4', 7); + $backend->set('test5', 11); + $backend->set('test6', 13); + $backend->set('test7', 17); + + $backend->delete('test1'); + $backend->delete('test23'); // Nonexistent key should not cause an error. + $backend->deleteMultiple(array( + 'test3', + 'test5', + 'test7', + 'test19', // Nonexistent key should not cause an error. + 'test21', // Nonexistent key should not cause an error. + )); + + // Test if expected keys have been deleted. + $this->assertIdentical(FALSE, $backend->get('test1'), "Cache id test1 deleted."); + $this->assertIdentical(FALSE, $backend->get('test3'), "Cache id test3 deleted."); + $this->assertIdentical(FALSE, $backend->get('test5'), "Cache id test5 deleted."); + $this->assertIdentical(FALSE, $backend->get('test7'), "Cache id test7 deleted."); + + // Test if expected keys exist. + $this->assertNotIdentical(FALSE, $backend->get('test2'), "Cache id test2 exists."); + $this->assertNotIdentical(FALSE, $backend->get('test4'), "Cache id test4 exists."); + $this->assertNotIdentical(FALSE, $backend->get('test6'), "Cache id test6 exists."); + + // Test if that expected keys do not exist. + $this->assertIdentical(FALSE, $backend->get('test19'), "Cache id test19 does not exist."); + $this->assertIdentical(FALSE, $backend->get('test21'), "Cache id test21 does not exist."); + + // Calling deleteMultiple() with an empty array should not cause an error. + $this->assertFalse($backend->deleteMultiple(array())); + } + + /** + * Test Drupal\Core\Cache\CacheBackendInterface::deleteAll(). + */ + public function testDeleteAll() { + $backend_a = $this->getCacheBackend(); + $backend_b = $this->getCacheBackend('bootstrap'); + + // Set both expiring and permanent keys. + $backend_a->set('test1', 1, Cache::PERMANENT); + $backend_a->set('test2', 3, time() + 1000); + $backend_b->set('test3', 4, Cache::PERMANENT); + + $backend_a->deleteAll(); + + $this->assertFalse($backend_a->get('test1'), 'First key has been deleted.'); + $this->assertFalse($backend_a->get('test2'), 'Second key has been deleted.'); + $this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.'); + } + + /** + * Test Drupal\Core\Cache\CacheBackendInterface::invalidate() and + * Drupal\Core\Cache\CacheBackendInterface::invalidateMultiple(). + */ + function testInvalidate() { + $backend = $this->getCacheBackend(); + $backend->set('test1', 1); + $backend->set('test2', 2); + $backend->set('test3', 2); + $backend->set('test4', 2); + + $reference = array('test1', 'test2', 'test3', 'test4'); + + $cids = $reference; + $ret = $backend->getMultiple($cids); + $this->assertEqual(count($ret), 4, 'Four items returned.'); + + $backend->invalidate('test1'); + $backend->invalidateMultiple(array('test2', 'test3')); + + $cids = $reference; + $ret = $backend->getMultiple($cids); + $this->assertEqual(count($ret), 1, 'Only one item element returned.'); + + $cids = $reference; + $ret = $backend->getMultiple($cids, TRUE); + $this->assertEqual(count($ret), 4, 'Four items returned.'); + + // Calling invalidateMultiple() with an empty array should not cause an + // error. + $this->assertFalse($backend->invalidateMultiple(array())); + } + + /** + * Tests Drupal\Core\Cache\CacheBackendInterface::invalidateTags(). + */ + function testInvalidateTags() { + $backend = $this->getCacheBackend(); + + // Create two cache entries with the same tag and tag value. + $backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:2')); + $backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:2')); + $this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Two cache items were created.'); + + // Invalidate test_tag of value 1. This should invalidate both entries. + Cache::invalidateTags(array('test_tag:2')); + $this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two cache items invalidated after invalidating a cache tag.'); + $this->assertTrue($backend->get('test_cid_invalidate1', TRUE) && $backend->get('test_cid_invalidate2', TRUE), 'Cache items not deleted after invalidating a cache tag.'); + + // Create two cache entries with the same tag and an array tag value. + $backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:1')); + $backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:1')); + $this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Two cache items were created.'); + + // Invalidate test_tag of value 1. This should invalidate both entries. + Cache::invalidateTags(array('test_tag:1')); + $this->assertFalse($backend->get('test_cid_invalidate1') || $backend->get('test_cid_invalidate2'), 'Two caches removed after invalidating a cache tag.'); + $this->assertTrue($backend->get('test_cid_invalidate1', TRUE) && $backend->get('test_cid_invalidate2', TRUE), 'Cache items not deleted after invalidating a cache tag.'); + + // Create three cache entries with a mix of tags and tag values. + $backend->set('test_cid_invalidate1', $this->defaultValue, Cache::PERMANENT, array('test_tag:1')); + $backend->set('test_cid_invalidate2', $this->defaultValue, Cache::PERMANENT, array('test_tag:2')); + $backend->set('test_cid_invalidate3', $this->defaultValue, Cache::PERMANENT, array('test_tag_foo:3')); + $this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2') && $backend->get('test_cid_invalidate3'), 'Three cached items were created.'); + Cache::invalidateTags(array('test_tag_foo:3')); + $this->assertTrue($backend->get('test_cid_invalidate1') && $backend->get('test_cid_invalidate2'), 'Cache items not matching the tag were not invalidated.'); + $this->assertFalse($backend->get('test_cid_invalidated3'), 'Cached item matching the tag was removed.'); + + // Create cache entry in multiple bins. Two cache entries + // (test_cid_invalidate1 and test_cid_invalidate2) still exist from previous + // tests. + $tags = array('test_tag:1', 'test_tag:2', 'test_tag:3'); + $bins = array('path', 'bootstrap', 'page'); + foreach ($bins as $bin) { + $this->getCacheBackend($bin)->set('test', $this->defaultValue, Cache::PERMANENT, $tags); + $this->assertTrue($this->getCacheBackend($bin)->get('test'), 'Cache item was set in bin.'); + } + + Cache::invalidateTags(array('test_tag:2')); + + // Test that the cache entry has been invalidated in multiple bins. + foreach ($bins as $bin) { + $this->assertFalse($this->getCacheBackend($bin)->get('test'), 'Tag invalidation affected item in bin.'); + } + // Test that the cache entry with a matching tag has been invalidated. + $this->assertFalse($this->getCacheBackend($bin)->get('test_cid_invalidate2'), 'Cache items matching tag were invalidated.'); + // Test that the cache entry with without a matching tag still exists. + $this->assertTrue($this->getCacheBackend($bin)->get('test_cid_invalidate1'), 'Cache items not matching tag were not invalidated.'); + } + + /** + * Test Drupal\Core\Cache\CacheBackendInterface::invalidateAll(). + */ + public function testInvalidateAll() { + $backend_a = $this->getCacheBackend(); + $backend_b = $this->getCacheBackend('bootstrap'); + + // Set both expiring and permanent keys. + $backend_a->set('test1', 1, Cache::PERMANENT); + $backend_a->set('test2', 3, time() + 1000); + $backend_b->set('test3', 4, Cache::PERMANENT); + + $backend_a->invalidateAll(); + + $this->assertFalse($backend_a->get('test1'), 'First key has been invalidated.'); + $this->assertFalse($backend_a->get('test2'), 'Second key has been invalidated.'); + $this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.'); + $this->assertTrue($backend_a->get('test1', TRUE), 'First key has not been deleted.'); + $this->assertTrue($backend_a->get('test2', TRUE), 'Second key has not been deleted.'); + } + + /** + * Tests Drupal\Core\Cache\CacheBackendInterface::removeBin(). + */ + public function testRemoveBin() { + $backend_a = $this->getCacheBackend(); + $backend_b = $this->getCacheBackend('bootstrap'); + + // Set both expiring and permanent keys. + $backend_a->set('test1', 1, Cache::PERMANENT); + $backend_a->set('test2', 3, time() + 1000); + $backend_b->set('test3', 4, Cache::PERMANENT); + + $backend_a->removeBin(); + + $this->assertFalse($backend_a->get('test1'), 'First key has been deleted.'); + $this->assertFalse($backend_a->get('test2', TRUE), 'Second key has been deleted.'); + $this->assertTrue($backend_b->get('test3'), 'Item in other bin is preserved.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/MemoryBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/MemoryBackendTest.php new file mode 100644 index 0000000..c1fb9f7 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/MemoryBackendTest.php @@ -0,0 +1,26 @@ +addInvalidator($backend); + return $backend; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/PhpBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/PhpBackendTest.php new file mode 100644 index 0000000..486d2d4 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Cache/PhpBackendTest.php @@ -0,0 +1,25 @@ +register('cache_factory', 'Drupal\Core\Cache\DatabaseBackendFactory') + ->addArgument(new Reference('database')) + ->addArgument(new Reference('cache_tags.invalidator.checksum')); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // Determine what database backend is running, and set the skip flag. + $this->skipTests = Database::getConnection()->databaseType() !== 'mysql'; + + // Create some schemas so our export contains tables. + $this->installSchema('system', [ + 'key_value_expire', + 'sessions', + ]); + $this->installSchema('dblog', ['watchdog']); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('user'); + $this->installEntitySchema('file'); + $this->installEntitySchema('menu_link_content'); + $this->installSchema('system', 'sequences'); + + // Place some sample config to test for in the export. + $this->data = [ + 'foo' => $this->randomMachineName(), + 'bar' => $this->randomMachineName(), + ]; + $storage = new DatabaseStorage(Database::getConnection(), 'config'); + $storage->write('test_config', $this->data); + + // Create user account with some potential syntax issues. + $account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']); + $account->save(); + + // Create url_alias (this will create 'url_alias'). + $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example'); + + // Create a cache table (this will create 'cache_discovery'). + \Drupal::cache('discovery')->set('test', $this->data); + + // These are all the tables that should now be in place. + $this->tables = [ + 'block_content', + 'block_content_field_data', + 'block_content_field_revision', + 'block_content_revision', + 'cachetags', + 'config', + 'cache_bootstrap', + 'cache_config', + 'cache_data', + 'cache_default', + 'cache_discovery', + 'cache_entity', + 'file_managed', + 'key_value_expire', + 'menu_link_content', + 'menu_link_content_data', + 'sequences', + 'sessions', + 'url_alias', + 'user__roles', + 'users', + 'users_field_data', + 'watchdog', + ]; + } + + /** + * Test the command directly. + */ + public function testDbDumpCommand() { + if ($this->skipTests) { + $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql"); + return; + } + + $application = new DbDumpApplication(); + $command = $application->find('dump-database-d8-mysql'); + $command_tester = new CommandTester($command); + $command_tester->execute([]); + + // Tables that are schema-only should not have data exported. + $pattern = preg_quote("\$connection->insert('sessions')"); + $this->assertFalse(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Tables defined as schema-only do not have data exported to the script.'); + + // Table data is exported. + $pattern = preg_quote("\$connection->insert('config')"); + $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Table data is properly exported to the script.'); + + // The test data are in the dump (serialized). + $pattern = preg_quote(serialize($this->data)); + $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'Generated data is found in the exported script.'); + + // Check that the user account name and email address was properly escaped. + $pattern = preg_quote('"q\'uote\$dollar@example.com"'); + $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account email address was properly escaped in the exported script.'); + $pattern = preg_quote('\'$dollar\''); + $this->assertTrue(preg_match('/' . $pattern . '/', $command_tester->getDisplay()), 'The user account name was properly escaped in the exported script.'); + } + + /** + * Test loading the script back into the database. + */ + public function testScriptLoad() { + if ($this->skipTests) { + $this->pass("Skipping test since the DbDumpCommand is currently only compatible with MySql"); + return; + } + + // Generate the script. + $application = new DbDumpApplication(); + $command = $application->find('dump-database-d8-mysql'); + $command_tester = new CommandTester($command); + $command_tester->execute([]); + $script = $command_tester->getDisplay(); + + // Store original schemas and drop tables to avoid errors. + foreach ($this->tables as $table) { + $this->originalTableSchemas[$table] = $this->getTableSchema($table); + $this->originalTableIndexes[$table] = $this->getTableIndexes($table); + Database::getConnection()->schema()->dropTable($table); + } + + // This will load the data. + $file = sys_get_temp_dir() . '/' . $this->randomMachineName(); + file_put_contents($file, $script); + require_once $file; + + // The tables should now exist and the schemas should match the originals. + foreach ($this->tables as $table) { + $this->assertTrue(Database::getConnection() + ->schema() + ->tableExists($table), SafeMarkup::format('Table @table created by the database script.', ['@table' => $table])); + $this->assertIdentical($this->originalTableSchemas[$table], $this->getTableSchema($table), SafeMarkup::format('The schema for @table was properly restored.', ['@table' => $table])); + $this->assertIdentical($this->originalTableIndexes[$table], $this->getTableIndexes($table), SafeMarkup::format('The indexes for @table were properly restored.', ['@table' => $table])); + } + + // Ensure the test config has been replaced. + $config = unserialize(db_query("SELECT data FROM {config} WHERE name = 'test_config'")->fetchField()); + $this->assertIdentical($config, $this->data, 'Script has properly restored the config table data.'); + + // Ensure the cache data was not exported. + $this->assertFalse(\Drupal::cache('discovery') + ->get('test'), 'Cache data was not exported to the script.'); + } + + /** + * Helper function to get a simplified schema for a given table. + * + * @param string $table + * + * @return array + * Array keyed by field name, with the values being the field type. + */ + protected function getTableSchema($table) { + // Verify the field type on the data column in the cache table. + // @todo this is MySQL specific. + $query = db_query("SHOW COLUMNS FROM {" . $table . "}"); + $definition = []; + while ($row = $query->fetchAssoc()) { + $definition[$row['Field']] = $row['Type']; + } + return $definition; + } + + /** + * Returns indexes for a given table. + * + * @param string $table + * The table to find indexes for. + * + * @return array + * The 'primary key', 'unique keys', and 'indexes' portion of the Drupal + * table schema. + */ + protected function getTableIndexes($table) { + $query = db_query("SHOW INDEX FROM {" . $table . "}"); + $definition = []; + while ($row = $query->fetchAssoc()) { + $index_name = $row['Key_name']; + $column = $row['Column_name']; + // Key the arrays by the index sequence for proper ordering (start at 0). + $order = $row['Seq_in_index'] - 1; + + // If specified, add length to the index. + if ($row['Sub_part']) { + $column = [$column, $row['Sub_part']]; + } + + if ($index_name === 'PRIMARY') { + $definition['primary key'][$order] = $column; + } + elseif ($row['Non_unique'] == 0) { + $definition['unique keys'][$index_name][$order] = $column; + } + else { + $definition['indexes'][$index_name][$order] = $column; + } + } + return $definition; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php b/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php new file mode 100644 index 0000000..2bcee75 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Common/SizeTest.php @@ -0,0 +1,69 @@ +exactTestCases = array( + '1 byte' => 1, + '1 KB' => $kb, + '1 MB' => $kb * $kb, + '1 GB' => $kb * $kb * $kb, + '1 TB' => $kb * $kb * $kb * $kb, + '1 PB' => $kb * $kb * $kb * $kb * $kb, + '1 EB' => $kb * $kb * $kb * $kb * $kb * $kb, + '1 ZB' => $kb * $kb * $kb * $kb * $kb * $kb * $kb, + '1 YB' => $kb * $kb * $kb * $kb * $kb * $kb * $kb * $kb, + ); + $this->roundedTestCases = array( + '2 bytes' => 2, + '1 MB' => ($kb * $kb) - 1, // rounded to 1 MB (not 1000 or 1024 kilobyte!) + round(3623651 / ($this->exactTestCases['1 MB']), 2) . ' MB' => 3623651, // megabytes + round(67234178751368124 / ($this->exactTestCases['1 PB']), 2) . ' PB' => 67234178751368124, // petabytes + round(235346823821125814962843827 / ($this->exactTestCases['1 YB']), 2) . ' YB' => 235346823821125814962843827, // yottabytes + ); + } + + /** + * Checks that format_size() returns the expected string. + */ + function testCommonFormatSize() { + foreach (array($this->exactTestCases, $this->roundedTestCases) as $test_cases) { + foreach ($test_cases as $expected => $input) { + $this->assertEqual( + ($result = format_size($input, NULL)), + $expected, + $expected . ' == ' . $result . ' (' . $input . ' bytes)' + ); + } + } + } + + /** + * Cross-tests Bytes::toInt() and format_size(). + */ + function testCommonParseSizeFormatSize() { + foreach ($this->exactTestCases as $size) { + $this->assertEqual( + $size, + ($parsed_size = Bytes::toInt($string = format_size($size, NULL))), + $size . ' == ' . $parsed_size . ' (' . $string . ')' + ); + } + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Common/XssUnitTest.php b/core/tests/Drupal/KernelTests/Core/Common/XssUnitTest.php new file mode 100644 index 0000000..7c68ed4 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Common/XssUnitTest.php @@ -0,0 +1,57 @@ +installConfig(array('system')); + } + + /** + * Tests t() functionality. + */ + function testT() { + $text = t('Simple text'); + $this->assertEqual($text, 'Simple text', 't leaves simple text alone.'); + $text = t('Escaped text: @value', array('@value' => '')) !== FALSE); + $this->assertTrue(strpos($response->getContent(), '') === FALSE); + } + + /** + * Tests exception message escaping. + */ + public function testExceptionEscaping() { + // Enable verbose error logging. + $this->config('system.logging')->set('error_level', ERROR_REPORTING_DISPLAY_VERBOSE)->save(); + + // Using SafeMarkup::format(). + $request = Request::create('/router_test/test24'); + $request->setFormat('html', ['text/html']); + + /** @var \Symfony\Component\HttpKernel\HttpKernelInterface $kernel */ + $kernel = \Drupal::getContainer()->get('http_kernel'); + $response = $kernel->handle($request)->prepare($request); + $this->assertEqual($response->getStatusCode(), Response::HTTP_INTERNAL_SERVER_ERROR); + $this->assertEqual($response->headers->get('Content-type'), 'text/html; charset=UTF-8'); + + // Test message is properly escaped, and that the unescaped string is not + // output at all. + $this->setRawContent($response->getContent()); + $this->assertRaw(Html::escape('Escaped content: ')); + $this->assertNoRaw(' '); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php b/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php new file mode 100644 index 0000000..7c06df0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Routing/MatcherDumperTest.php @@ -0,0 +1,168 @@ +fixtures = new RoutingFixtures(); + $this->state = new State(new KeyValueMemoryFactory()); + } + + /** + * Confirms that the dumper can be instantiated successfully. + */ + function testCreate() { + $connection = Database::getConnection(); + $dumper = new MatcherDumper($connection, $this->state); + + $class_name = 'Drupal\Core\Routing\MatcherDumper'; + $this->assertTrue($dumper instanceof $class_name, 'Dumper created successfully'); + } + + /** + * Confirms that we can add routes to the dumper. + */ + function testAddRoutes() { + $connection = Database::getConnection(); + $dumper = new MatcherDumper($connection, $this->state); + + $route = new Route('test'); + $collection = new RouteCollection(); + $collection->add('test_route', $route); + + $dumper->addRoutes($collection); + + $dumper_routes = $dumper->getRoutes()->all(); + $collection_routes = $collection->all(); + + foreach ($dumper_routes as $name => $route) { + $this->assertEqual($route->getPath(), $collection_routes[$name]->getPath(), 'Routes match'); + } + } + + /** + * Confirms that we can add routes to the dumper when it already has some. + */ + function testAddAdditionalRoutes() { + $connection = Database::getConnection(); + $dumper = new MatcherDumper($connection, $this->state); + + $route = new Route('test'); + $collection = new RouteCollection(); + $collection->add('test_route', $route); + $dumper->addRoutes($collection); + + $route = new Route('test2'); + $collection2 = new RouteCollection(); + $collection2->add('test_route2', $route); + $dumper->addRoutes($collection2); + + // Merge the two collections together so we can test them. + $collection->addCollection(clone $collection2); + + $dumper_routes = $dumper->getRoutes()->all(); + $collection_routes = $collection->all(); + + $success = TRUE; + foreach ($collection_routes as $name => $route) { + if (empty($dumper_routes[$name])) { + $success = FALSE; + $this->fail(t('Not all routes found in the dumper.')); + } + } + + if ($success) { + $this->pass('All routes found in the dumper.'); + } + } + + /** + * Confirm that we can dump a route collection to the database. + */ + public function testDump() { + $connection = Database::getConnection(); + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + + $route = new Route('/test/{my}/path'); + $route->setOption('compiler_class', 'Drupal\Core\Routing\RouteCompiler'); + $collection = new RouteCollection(); + $collection->add('test_route', $route); + + $dumper->addRoutes($collection); + + $this->fixtures->createTables($connection); + + $dumper->dump(array('provider' => 'test')); + + $record = $connection->query("SELECT * FROM {test_routes} WHERE name= :name", array(':name' => 'test_route'))->fetchObject(); + + $loaded_route = unserialize($record->route); + + $this->assertEqual($record->name, 'test_route', 'Dumped route has correct name.'); + $this->assertEqual($record->path, '/test/{my}/path', 'Dumped route has correct pattern.'); + $this->assertEqual($record->pattern_outline, '/test/%/path', 'Dumped route has correct pattern outline.'); + $this->assertEqual($record->fit, 5 /* 101 in binary */, 'Dumped route has correct fit.'); + $this->assertTrue($loaded_route instanceof Route, 'Route object retrieved successfully.'); + } + + /** + * Tests the determination of the masks generation. + */ + public function testMenuMasksGeneration() { + $connection = Database::getConnection(); + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + + $collection = new RouteCollection(); + $collection->add('test_route_1', new Route('/test-length-3/{my}/path')); + $collection->add('test_route_2', new Route('/test-length-3/hello/path')); + $collection->add('test_route_3', new Route('/test-length-5/{my}/path/marvin/magrathea')); + $collection->add('test_route_4', new Route('/test-length-7/{my}/path/marvin/magrathea/earth/ursa-minor')); + + $dumper->addRoutes($collection); + + $this->fixtures->createTables($connection); + + $dumper->dump(array('provider' => 'test')); + // Using binary for readability, we expect a 0 at any wildcard slug. They + // should be ordered from longest to shortest. + $expected = array( + bindec('1011111'), + bindec('10111'), + bindec('111'), + bindec('101'), + ); + $this->assertEqual($this->state->get('routing.menu_masks.test_routes'), $expected); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php new file mode 100644 index 0000000..d28ca5e --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -0,0 +1,630 @@ +fixtures = new RoutingFixtures(); + $this->state = new State(new KeyValueMemoryFactory()); + $this->currentPath = new CurrentPathStack(new RequestStack()); + $this->cache = new MemoryBackend('data'); + $this->pathProcessor = \Drupal::service('path_processor_manager'); + $this->cacheTagsInvalidator = \Drupal::service('cache_tags.invalidator'); + } + + /** + * {@inheritdoc} + */ + public function register(ContainerBuilder $container) { + parent::register($container); + + // Readd the incoming path alias for these tests. + if ($container->hasDefinition('path_processor_alias')) { + $definition = $container->getDefinition('path_processor_alias'); + $definition->addTag('path_processor_inbound'); + } + } + + protected function tearDown() { + $this->fixtures->dropTables(Database::getConnection()); + + parent::tearDown(); + } + + /** + * Confirms that the correct candidate outlines are generated. + */ + public function testCandidateOutlines() { + + $connection = Database::getConnection(); + $provider = new TestRouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $parts = array('node', '5', 'edit'); + + $candidates = $provider->getCandidateOutlines($parts); + + $candidates = array_flip($candidates); + + $this->assertTrue(count($candidates) == 7, 'Correct number of candidates found'); + $this->assertTrue(array_key_exists('/node/5/edit', $candidates), 'First candidate found.'); + $this->assertTrue(array_key_exists('/node/5/%', $candidates), 'Second candidate found.'); + $this->assertTrue(array_key_exists('/node/%/edit', $candidates), 'Third candidate found.'); + $this->assertTrue(array_key_exists('/node/%/%', $candidates), 'Fourth candidate found.'); + $this->assertTrue(array_key_exists('/node/5', $candidates), 'Fifth candidate found.'); + $this->assertTrue(array_key_exists('/node/%', $candidates), 'Sixth candidate found.'); + $this->assertTrue(array_key_exists('/node', $candidates), 'Seventh candidate found.'); + } + + /** + * Don't fail when given an empty path. + */ + public function testEmptyPathCandidatesOutlines() { + $provider = new TestRouteProvider(Database::getConnection(), $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + $candidates = $provider->getCandidateOutlines([]); + $this->assertEqual(count($candidates), 0, 'Empty parts should return no candidates.'); + } + + /** + * Confirms that we can find routes with the exact incoming path. + */ + function testExactPathMatch() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->sampleRouteCollection()); + $dumper->dump(); + + $path = '/path/one'; + + $request = Request::create($path, 'GET'); + + $routes = $provider->getRouteCollectionForRequest($request); + + foreach ($routes as $route) { + $this->assertEqual($route->getPath(), $path, 'Found path has correct pattern'); + } + } + + /** + * Confirms that we can find routes whose pattern would match the request. + */ + function testOutlinePathMatch() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->complexRouteCollection()); + $dumper->dump(); + + $path = '/path/1/one'; + + $request = Request::create($path, 'GET'); + + $routes = $provider->getRouteCollectionForRequest($request); + + // All of the matching paths have the correct pattern. + foreach ($routes as $route) { + $this->assertEqual($route->compile()->getPatternOutline(), '/path/%/one', 'Found path has correct pattern'); + } + + $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); + $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.'); + $this->assertNotNull($routes->get('route_b'), 'The second matching route was not found.'); + } + + /** + * Confirms that a trailing slash on the request doesn't result in a 404. + */ + function testOutlinePathMatchTrailingSlash() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->complexRouteCollection()); + $dumper->dump(); + + $path = '/path/1/one/'; + + $request = Request::create($path, 'GET'); + + $routes = $provider->getRouteCollectionForRequest($request); + + // All of the matching paths have the correct pattern. + foreach ($routes as $route) { + $this->assertEqual($route->compile()->getPatternOutline(), '/path/%/one', 'Found path has correct pattern'); + } + + $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); + $this->assertNotNull($routes->get('route_a'), 'The first matching route was found.'); + $this->assertNotNull($routes->get('route_b'), 'The second matching route was not found.'); + } + + /** + * Confirms that we can find routes whose pattern would match the request. + */ + function testOutlinePathMatchDefaults() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $collection = new RouteCollection(); + $collection->add('poink', new Route('/some/path/{value}', array( + 'value' => 'poink', + ))); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(); + + $path = '/some/path'; + + $request = Request::create($path, 'GET'); + + try { + $routes = $provider->getRouteCollectionForRequest($request); + + // All of the matching paths have the correct pattern. + foreach ($routes as $route) { + $this->assertEqual($route->compile()->getPatternOutline(), '/some/path', 'Found path has correct pattern'); + } + + $this->assertEqual(count($routes), 1, 'The correct number of routes was found.'); + $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); + } + catch (ResourceNotFoundException $e) { + $this->fail('No matching route found with default argument value.'); + } + } + + /** + * Confirms that we can find routes whose pattern would match the request. + */ + function testOutlinePathMatchDefaultsCollision() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $collection = new RouteCollection(); + $collection->add('poink', new Route('/some/path/{value}', array( + 'value' => 'poink', + ))); + $collection->add('narf', new Route('/some/path/here')); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(); + + $path = '/some/path'; + + $request = Request::create($path, 'GET'); + + try { + $routes = $provider->getRouteCollectionForRequest($request); + + // All of the matching paths have the correct pattern. + foreach ($routes as $route) { + $this->assertEqual($route->compile()->getPatternOutline(), '/some/path', 'Found path has correct pattern'); + } + + $this->assertEqual(count($routes), 1, 'The correct number of routes was found.'); + $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); + } + catch (ResourceNotFoundException $e) { + $this->fail('No matching route found with default argument value.'); + } + } + + /** + * Confirms that we can find routes whose pattern would match the request. + */ + function testOutlinePathMatchDefaultsCollision2() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $collection = new RouteCollection(); + $collection->add('poink', new Route('/some/path/{value}', array( + 'value' => 'poink', + ))); + $collection->add('narf', new Route('/some/path/here')); + $collection->add('eep', new Route('/something/completely/different')); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(); + + $path = '/some/path/here'; + + $request = Request::create($path, 'GET'); + + try { + $routes = $provider->getRouteCollectionForRequest($request); + $routes_array = $routes->all(); + + $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); + $this->assertEqual(array('narf', 'poink'), array_keys($routes_array), 'Ensure the fitness was taken into account.'); + $this->assertNotNull($routes->get('narf'), 'The first matching route was found.'); + $this->assertNotNull($routes->get('poink'), 'The second matching route was found.'); + $this->assertNull($routes->get('eep'), 'Non-matching route was not found.'); + } + catch (ResourceNotFoundException $e) { + $this->fail('No matching route found with default argument value.'); + } + } + + /** + * Confirms that we can find multiple routes that match the request equally. + */ + function testOutlinePathMatchDefaultsCollision3() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $collection = new RouteCollection(); + $collection->add('poink', new Route('/some/{value}/path')); + // Add a second route matching the same path pattern. + $collection->add('poink2', new Route('/some/{object}/path')); + $collection->add('narf', new Route('/some/here/path')); + $collection->add('eep', new Route('/something/completely/different')); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(); + + $path = '/some/over-there/path'; + + $request = Request::create($path, 'GET'); + + try { + $routes = $provider->getRouteCollectionForRequest($request); + $routes_array = $routes->all(); + + $this->assertEqual(count($routes), 2, 'The correct number of routes was found.'); + $this->assertEqual(array('poink', 'poink2'), array_keys($routes_array), 'Ensure the fitness and name were taken into account in the sort.'); + $this->assertNotNull($routes->get('poink'), 'The first matching route was found.'); + $this->assertNotNull($routes->get('poink2'), 'The second matching route was found.'); + $this->assertNull($routes->get('eep'), 'Non-matching route was not found.'); + } + catch (ResourceNotFoundException $e) { + $this->fail('No matching route found with default argument value.'); + } + } + + /** + * Tests a route with a 0 as value. + */ + public function testOutlinePathMatchZero() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $collection = new RouteCollection(); + $collection->add('poink', new Route('/some/path/{value}')); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($collection); + $dumper->dump(); + + $path = '/some/path/0'; + + $request = Request::create($path, 'GET'); + + try { + $routes = $provider->getRouteCollectionForRequest($request); + + // All of the matching paths have the correct pattern. + foreach ($routes as $route) { + $this->assertEqual($route->compile()->getPatternOutline(), '/some/path/%', 'Found path has correct pattern'); + } + + $this->assertEqual(count($routes), 1, 'The correct number of routes was found.'); + } + catch (ResourceNotFoundException $e) { + $this->fail('No matchout route found with 0 as argument value'); + } + } + + /** + * Confirms that an exception is thrown when no matching path is found. + */ + function testOutlinePathNoMatch() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->complexRouteCollection()); + $dumper->dump(); + + $path = '/no/such/path'; + + $request = Request::create($path, 'GET'); + + + $routes = $provider->getRoutesByPattern($path); + $this->assertFalse(count($routes), 'No path found with this pattern.'); + + $collection = $provider->getRouteCollectionForRequest($request); + $this->assertTrue(count($collection) == 0, 'Empty route collection found with this pattern.'); + } + + /** + * Tests that route caching works. + */ + public function testRouteCaching() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->sampleRouteCollection()); + $dumper->addRoutes($this->fixtures->complexRouteCollection()); + $dumper->dump(); + + // A simple path. + $path = '/path/add/one'; + $request = Request::create($path, 'GET'); + $provider->getRouteCollectionForRequest($request); + + $cache = $this->cache->get('route:/path/add/one:'); + $this->assertEqual('/path/add/one', $cache->data['path']); + $this->assertEqual([], $cache->data['query']); + $this->assertEqual(3, count($cache->data['routes'])); + + // A path with query parameters. + $path = '/path/add/one?foo=bar'; + $request = Request::create($path, 'GET'); + $provider->getRouteCollectionForRequest($request); + + $cache = $this->cache->get('route:/path/add/one:foo=bar'); + $this->assertEqual('/path/add/one', $cache->data['path']); + $this->assertEqual(['foo' => 'bar'], $cache->data['query']); + $this->assertEqual(3, count($cache->data['routes'])); + + // A path with placeholders. + $path = '/path/1/one'; + $request = Request::create($path, 'GET'); + $provider->getRouteCollectionForRequest($request); + + $cache = $this->cache->get('route:/path/1/one:'); + $this->assertEqual('/path/1/one', $cache->data['path']); + $this->assertEqual([], $cache->data['query']); + $this->assertEqual(2, count($cache->data['routes'])); + + // A path with a path alias. + /** @var \Drupal\Core\Path\AliasStorageInterface $path_storage */ + $path_storage = \Drupal::service('path.alias_storage'); + $path_storage->save('/path/add/one', '/path/add-one'); + /** @var \Drupal\Core\Path\AliasManagerInterface $alias_manager */ + $alias_manager = \Drupal::service('path.alias_manager'); + $alias_manager->cacheClear(); + + $path = '/path/add-one'; + $request = Request::create($path, 'GET'); + $provider->getRouteCollectionForRequest($request); + + $cache = $this->cache->get('route:/path/add-one:'); + $this->assertEqual('/path/add/one', $cache->data['path']); + $this->assertEqual([], $cache->data['query']); + $this->assertEqual(3, count($cache->data['routes'])); + } + + /** + * Test RouteProvider::getRouteByName() and RouteProvider::getRoutesByNames(). + */ + public function testRouteByName() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->sampleRouteCollection()); + $dumper->dump(); + + $route = $provider->getRouteByName('route_a'); + $this->assertEqual($route->getPath(), '/path/one', 'The right route pattern was found.'); + $this->assertEqual($route->getMethods(), ['GET'], 'The right route method was found.'); + $route = $provider->getRouteByName('route_b'); + $this->assertEqual($route->getPath(), '/path/one', 'The right route pattern was found.'); + $this->assertEqual($route->getMethods(), ['PUT'], 'The right route method was found.'); + + $exception_thrown = FALSE; + try { + $provider->getRouteByName('invalid_name'); + } + catch (RouteNotFoundException $e) { + $exception_thrown = TRUE; + } + $this->assertTrue($exception_thrown, 'Random route was not found.'); + + $routes = $provider->getRoutesByNames(array('route_c', 'route_d', $this->randomMachineName())); + $this->assertEqual(count($routes), 2, 'Only two valid routes found.'); + $this->assertEqual($routes['route_c']->getPath(), '/path/two'); + $this->assertEqual($routes['route_d']->getPath(), '/path/three'); + } + + /** + * Ensures that the routing system is capable of extreme long patterns. + */ + public function testGetRoutesByPatternWithLongPatterns() { + $connection = Database::getConnection(); + $provider = new TestRouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + // This pattern has only 3 parts, so we will get candidates, but no routes, + // even though we have not dumped the routes yet. + $shortest = '/test/1/test2'; + $result = $provider->getRoutesByPattern($shortest); + $this->assertEqual($result->count(), 0); + $candidates = $provider->getCandidateOutlines(explode('/', trim($shortest, '/'))); + $this->assertEqual(count($candidates), 7); + // A longer patten is not found and returns no candidates + $path_to_test = '/test/1/test2/2/test3/3/4/5/6/test4'; + $result = $provider->getRoutesByPattern($path_to_test); + $this->assertEqual($result->count(), 0); + $candidates = $provider->getCandidateOutlines(explode('/', trim($path_to_test, '/'))); + $this->assertEqual(count($candidates), 0); + + // Add a matching route and dump it. + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $collection = new RouteCollection(); + $collection->add('long_pattern', new Route('/test/{v1}/test2/{v2}/test3/{v3}/{v4}/{v5}/{v6}/test4')); + $dumper->addRoutes($collection); + $dumper->dump(); + + $result = $provider->getRoutesByPattern($path_to_test); + $this->assertEqual($result->count(), 1); + // We can't compare the values of the routes directly, nor use + // spl_object_hash() because they are separate instances. + $this->assertEqual(serialize($result->get('long_pattern')), serialize($collection->get('long_pattern')), 'The right route was found.'); + // We now have a single candidate outline. + $candidates = $provider->getCandidateOutlines(explode('/', trim($path_to_test, '/'))); + $this->assertEqual(count($candidates), 1); + // Longer and shorter patterns are not found. Both are longer than 3, so + // we should not have any candidates either. The fact that we do not + // get any candidates for a longer path is a security feature. + $longer = '/test/1/test2/2/test3/3/4/5/6/test4/trailing/more/parts'; + $result = $provider->getRoutesByPattern($longer); + $this->assertEqual($result->count(), 0); + $candidates = $provider->getCandidateOutlines(explode('/', trim($longer, '/'))); + $this->assertEqual(count($candidates), 1); + $shorter = '/test/1/test2/2/test3'; + $result = $provider->getRoutesByPattern($shorter); + $this->assertEqual($result->count(), 0); + $candidates = $provider->getCandidateOutlines(explode('/', trim($shorter, '/'))); + $this->assertEqual(count($candidates), 0); + // This pattern has only 3 parts, so we will get candidates, but no routes. + // This result is unchanged by running the dumper. + $result = $provider->getRoutesByPattern($shortest); + $this->assertEqual($result->count(), 0); + $candidates = $provider->getCandidateOutlines(explode('/', trim($shortest, '/'))); + $this->assertEqual(count($candidates), 7); + } + + /** + * Tests getRoutesPaged(). + */ + public function testGetRoutesPaged() { + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->sampleRouteCollection()); + $dumper->dump(); + + $fixture_routes = $this->fixtures->staticSampleRouteCollection(); + + // Query all the routes. + $routes = $provider->getRoutesPaged(0); + $this->assertEqual(array_keys($routes), array_keys($fixture_routes)); + + // Query non routes. + $routes = $provider->getRoutesPaged(0, 0); + $this->assertEqual(array_keys($routes), []); + + // Query a limited sets of routes. + $routes = $provider->getRoutesPaged(1, 2); + $this->assertEqual(array_keys($routes), array_slice(array_keys($fixture_routes), 1, 2)); + } + +} + +class TestRouteProvider extends RouteProvider { + + public function getCandidateOutlines(array $parts) { + return parent::getCandidateOutlines($parts); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Routing/UrlIntegrationTest.php b/core/tests/Drupal/KernelTests/Core/Routing/UrlIntegrationTest.php new file mode 100644 index 0000000..a70cc1f --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Routing/UrlIntegrationTest.php @@ -0,0 +1,53 @@ + 'role_with_access']); + $role_with_access->grantPermission('administer users'); + $role_with_access->save(); + + /** @var \Drupal\user\RoleInterface $role_without_access */ + $role_without_access = Role::create(['id' => 'role_without_access']); + $role_without_access->save(); + + $user_with_access = User::create(['roles' => ['role_with_access']]); + $user_without_access = User::create(['roles' => ['role_without_access']]); + + $url_always_access = new Url('router_test.1'); + $this->assertTrue($url_always_access->access($user_with_access)); + $this->assertTrue($url_always_access->access($user_without_access)); + + $url_none_access = new Url('router_test.15'); + $this->assertFalse($url_none_access->access($user_with_access)); + $this->assertFalse($url_none_access->access($user_without_access)); + + $url_access = new Url('router_test.16'); + $this->assertTrue($url_access->access($user_with_access)); + $this->assertFalse($url_access->access($user_without_access)); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/ServiceProvider/ServiceProviderTest.php b/core/tests/Drupal/KernelTests/Core/ServiceProvider/ServiceProviderTest.php new file mode 100644 index 0000000..2995c97 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/ServiceProvider/ServiceProviderTest.php @@ -0,0 +1,43 @@ +container->getDefinition('file.usage'); + $this->assertTrue($definition->getClass() == 'Drupal\\service_provider_test\\TestFileUsage', 'Class has been changed'); + $this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service has been registered to the DIC'); + } + + /** + * Tests that the DIC keeps up with module enable/disable in the same request. + */ + function testServiceProviderRegistrationDynamic() { + // Uninstall the module and ensure the service provider's service is not registered. + \Drupal::service('module_installer')->uninstall(array('service_provider_test')); + $this->assertFalse(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service does not exist in the DIC.'); + + // Install the module and ensure the service provider's service is registered. + \Drupal::service('module_installer')->install(array('service_provider_test')); + $this->assertTrue(\Drupal::hasService('service_provider_test_class'), 'The service_provider_test_class service exists in the DIC.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Session/AccountSwitcherTest.php b/core/tests/Drupal/KernelTests/Core/Session/AccountSwitcherTest.php new file mode 100644 index 0000000..0e6fa8a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Session/AccountSwitcherTest.php @@ -0,0 +1,67 @@ +container->get('session_handler.write_safe'); + $user = $this->container->get('current_user'); + $switcher = $this->container->get('account_switcher'); + $original_user = $user->getAccount(); + $original_session_saving = $session_handler->isSessionWritable(); + + // Switch to user with uid 2. + $switcher->switchTo(new UserSession(array('uid' => 2))); + + // Verify that the active user has changed, and that session saving is + // disabled. + $this->assertEqual($user->id(), 2, 'Switched to user 2.'); + $this->assertFalse($session_handler->isSessionWritable(), 'Session saving is disabled.'); + + // Perform a second (nested) user account switch. + $switcher->switchTo(new UserSession(array('uid' => 3))); + $this->assertEqual($user->id(), 3, 'Switched to user 3.'); + + // Revert to the user session that was active between the first and second + // switch. + $switcher->switchBack(); + + // Since we are still in the account from the first switch, session handling + // still needs to be disabled. + $this->assertEqual($user->id(), 2, 'Reverted to user 2.'); + $this->assertFalse($session_handler->isSessionWritable(), 'Session saving still disabled.'); + + // Revert to the original account which was active before the first switch. + $switcher->switchBack(); + + // Assert that the original account is active again, and that session saving + // has been re-enabled. + $this->assertEqual($user->id(), $original_user->id(), 'Original user correctly restored.'); + $this->assertEqual($session_handler->isSessionWritable(), $original_session_saving, 'Original session saving correctly restored.'); + + // Verify that AccountSwitcherInterface::switchBack() will throw + // an exception if there are no accounts left in the stack. + try { + $switcher->switchBack(); + $this->fail('::switchBack() throws exception if called without previous switch.'); + } + catch (\RuntimeException $e) { + if ($e->getMessage() == 'No more accounts to revert to.') { + $this->pass('::switchBack() throws exception if called without previous switch.'); + } + else { + $this->fail($e->getMessage()); + } + } + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Site/SettingsRewriteTest.php b/core/tests/Drupal/KernelTests/Core/Site/SettingsRewriteTest.php new file mode 100644 index 0000000..948236b --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Site/SettingsRewriteTest.php @@ -0,0 +1,127 @@ +container->get('site.path'); + $tests = array( + array( + 'original' => '$no_index_value_scalar = TRUE;', + 'settings' => array( + 'no_index_value_scalar' => (object) array( + 'value' => FALSE, + 'comment' => 'comment', + ), + ), + 'expected' => '$no_index_value_scalar = false; // comment', + ), + array( + 'original' => '$no_index_value_scalar = TRUE;', + 'settings' => array( + 'no_index_value_foo' => array( + 'foo' => array( + 'value' => (object) array( + 'value' => NULL, + 'required' => TRUE, + 'comment' => 'comment', + ), + ), + ), + ), + 'expected' => <<<'EXPECTED' +$no_index_value_scalar = TRUE; +$no_index_value_foo['foo']['value'] = NULL; // comment +EXPECTED + ), + array( + 'original' => '$no_index_value_array = array("old" => "value");', + 'settings' => array( + 'no_index_value_array' => (object) array( + 'value' => FALSE, + 'required' => TRUE, + 'comment' => 'comment', + ), + ), + 'expected' => '$no_index_value_array = array("old" => "value"); +$no_index_value_array = false; // comment', + ), + array( + 'original' => '$has_index_value_scalar["foo"]["bar"] = NULL;', + 'settings' => array( + 'has_index_value_scalar' => array( + 'foo' => array( + 'bar' => (object) array( + 'value' => FALSE, + 'required' => TRUE, + 'comment' => 'comment', + ), + ), + ), + ), + 'expected' => '$has_index_value_scalar["foo"]["bar"] = false; // comment', + ), + array( + 'original' => '$has_index_value_scalar["foo"]["bar"] = "foo";', + 'settings' => array( + 'has_index_value_scalar' => array( + 'foo' => array( + 'value' => (object) array( + 'value' => array('value' => 2), + 'required' => TRUE, + 'comment' => 'comment', + ), + ), + ), + ), + 'expected' => <<<'EXPECTED' +$has_index_value_scalar["foo"]["bar"] = "foo"; +$has_index_value_scalar['foo']['value'] = array ( + 'value' => 2, +); // comment +EXPECTED + ), + ); + foreach ($tests as $test) { + $filename = Settings::get('file_public_path', $site_path . '/files') . '/mock_settings.php'; + file_put_contents($filename, "assertEqual(file_get_contents($filename), " array( + 'no_index' => (object) array( + 'value' => TRUE, + 'required' => TRUE, + ), + ), + 'expected' => '$no_index = true;' + ); + // Make an empty file. + $filename = Settings::get('file_public_path', $site_path . '/files') . '/mock_settings.php'; + file_put_contents($filename, ""); + + // Write the setting to the file. + drupal_rewrite_settings($test['settings'], $filename); + + // Check that the result is just the php opening tag and the settings. + $this->assertEqual(file_get_contents($filename), "container = \Drupal::service('kernel')->getContainer(); + $this->container->get('request_stack')->push($request); + + $this->testImages = array( + 'core/misc/druplicon.png', + 'core/misc/loading.gif', + ); + } + + /** + * Tests that an image with the sizes attribute is output correctly. + */ + function testThemeImageWithSizes() { + // Test with multipliers. + $sizes = '(max-width: ' . rand(10, 30) . 'em) 100vw, (max-width: ' . rand(30, 50) . 'em) 50vw, 30vw'; + $image = array( + '#theme' => 'image', + '#sizes' => $sizes, + '#uri' => reset($this->testImages), + '#width' => rand(0, 1000) . 'px', + '#height' => rand(0, 500) . 'px', + '#alt' => $this->randomMachineName(), + '#title' => $this->randomMachineName(), + ); + $this->render($image); + + // Make sure sizes is set. + $this->assertRaw($sizes, 'Sizes is set correctly.'); + } + + /** + * Tests that an image with the src attribute is output correctly. + */ + function testThemeImageWithSrc() { + + $image = array( + '#theme' => 'image', + '#uri' => reset($this->testImages), + '#width' => rand(0, 1000) . 'px', + '#height' => rand(0, 500) . 'px', + '#alt' => $this->randomMachineName(), + '#title' => $this->randomMachineName(), + ); + $this->render($image); + + // Make sure the src attribute has the correct value. + $this->assertRaw(file_url_transform_relative(file_create_url($image['#uri'])), 'Correct output for an image with the src attribute.'); + } + + /** + * Tests that an image with the srcset and multipliers is output correctly. + */ + function testThemeImageWithSrcsetMultiplier() { + // Test with multipliers. + $image = array( + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => $this->testImages[0], + 'multiplier' => '1x', + ), + array( + 'uri' => $this->testImages[1], + 'multiplier' => '2x', + ), + ), + '#width' => rand(0, 1000) . 'px', + '#height' => rand(0, 500) . 'px', + '#alt' => $this->randomMachineName(), + '#title' => $this->randomMachineName(), + ); + $this->render($image); + + // Make sure the srcset attribute has the correct value. + $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' 1x, ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' 2x', 'Correct output for image with srcset attribute and multipliers.'); + } + + /** + * Tests that an image with the srcset and widths is output correctly. + */ + function testThemeImageWithSrcsetWidth() { + // Test with multipliers. + $widths = array( + rand(0, 500) . 'w', + rand(500, 1000) . 'w', + ); + $image = array( + '#theme' => 'image', + '#srcset' => array( + array( + 'uri' => $this->testImages[0], + 'width' => $widths[0], + ), + array( + 'uri' => $this->testImages[1], + 'width' => $widths[1], + ), + ), + '#width' => rand(0, 1000) . 'px', + '#height' => rand(0, 500) . 'px', + '#alt' => $this->randomMachineName(), + '#title' => $this->randomMachineName(), + ); + $this->render($image); + + // Make sure the srcset attribute has the correct value. + $this->assertRaw(file_url_transform_relative(file_create_url($this->testImages[0])) . ' ' . $widths[0] . ', ' . file_url_transform_relative(file_create_url($this->testImages[1])) . ' ' . $widths[1], 'Correct output for image with srcset attribute and width descriptors.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/MessageTest.php b/core/tests/Drupal/KernelTests/Core/Theme/MessageTest.php new file mode 100644 index 0000000..76e14e9 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/MessageTest.php @@ -0,0 +1,37 @@ +install(['classy']); + \Drupal::service('theme_handler')->setDefault('classy'); + + drupal_set_message('An error occurred', 'error'); + drupal_set_message('But then something nice happened'); + $messages = array( + '#type' => 'status_messages', + ); + $this->render($messages); + $this->assertRaw('messages messages--error'); + $this->assertRaw('messages messages--status'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php new file mode 100644 index 0000000..93e931b --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/RegistryTest.php @@ -0,0 +1,160 @@ +setMethod('GET'); + $cid = 'test_theme_registry'; + + // Directly instantiate the theme registry, this will cause a base cache + // entry to be written in __construct(). + $cache = \Drupal::cache(); + $lock_backend = \Drupal::lock(); + $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); + + $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); + + // Trigger a cache miss for an offset. + $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry.'); + // This will cause the ThemeRegistry class to write an updated version of + // the cache entry when it is destroyed, usually at the end of the request. + // Before that happens, manually delete the cache entry we created earlier + // so that the new entry is written from scratch. + \Drupal::cache()->delete($cid); + + // Destroy the class so that it triggers a cache write for the offset. + $registry->destruct(); + + $this->assertTrue(\Drupal::cache()->get($cid), 'Cache entry was created.'); + + // Create a new instance of the class. Confirm that both the offset + // requested previously, and one that has not yet been requested are both + // available. + $registry = new ThemeRegistry($cid, $cache, $lock_backend, array('theme_registry'), $this->container->get('module_handler')->isLoaded()); + $this->assertTrue($registry->get('theme_test_template_test'), 'Offset was returned correctly from the theme registry'); + $this->assertTrue($registry->get('theme_test_template_test_2'), 'Offset was returned correctly from the theme registry'); + } + + /** + * Tests the theme registry with multiple subthemes. + */ + public function testMultipleSubThemes() { + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_basetheme', 'test_subtheme', 'test_subsubtheme']); + + $registry_subsub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subsubtheme'); + $registry_subsub_theme->setThemeManager(\Drupal::theme()); + $registry_sub_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_subtheme'); + $registry_sub_theme->setThemeManager(\Drupal::theme()); + $registry_base_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_basetheme'); + $registry_base_theme->setThemeManager(\Drupal::theme()); + + $preprocess_functions = $registry_subsub_theme->get()['theme_test_template_test']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess', + 'test_basetheme_preprocess_theme_test_template_test', + 'test_subtheme_preprocess_theme_test_template_test', + 'test_subsubtheme_preprocess_theme_test_template_test', + ], $preprocess_functions); + + $preprocess_functions = $registry_sub_theme->get()['theme_test_template_test']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess', + 'test_basetheme_preprocess_theme_test_template_test', + 'test_subtheme_preprocess_theme_test_template_test', + ], $preprocess_functions); + + $preprocess_functions = $registry_base_theme->get()['theme_test_template_test']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess', + 'test_basetheme_preprocess_theme_test_template_test', + ], $preprocess_functions); + + $preprocess_functions = $registry_base_theme->get()['theme_test_function_suggestions']['preprocess functions']; + $this->assertIdentical([ + 'template_preprocess_theme_test_function_suggestions', + 'test_basetheme_preprocess_theme_test_function_suggestions', + ], $preprocess_functions, "Theme functions don't have template_preprocess but do have template_preprocess_HOOK"); + } + + /** + * Tests the theme registry with suggestions. + */ + public function testSuggestionPreprocessFunctions() { + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_theme']); + + $registry_theme = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); + $registry_theme->setThemeManager(\Drupal::theme()); + + $suggestions = ['__kitten', '__flamingo']; + $expected_preprocess_functions = [ + 'template_preprocess', + 'theme_test_preprocess_theme_test_preprocess_suggestions', + ]; + $suggestion = ''; + $hook = 'theme_test_preprocess_suggestions'; + do { + $hook .= "$suggestion"; + $expected_preprocess_functions[] = "test_theme_preprocess_$hook"; + $preprocess_functions = $registry_theme->get()[$hook]['preprocess functions']; + $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, "$hook has correct preprocess functions."); + } while ($suggestion = array_shift($suggestions)); + + $expected_preprocess_functions = [ + 'template_preprocess', + 'theme_test_preprocess_theme_test_preprocess_suggestions', + 'test_theme_preprocess_theme_test_preprocess_suggestions', + 'test_theme_preprocess_theme_test_preprocess_suggestions__kitten', + ]; + + $preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat']['preprocess functions']; + $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a function correctly inherits preprocess functions.'); + + $preprocess_functions = $registry_theme->get()['theme_test_preprocess_suggestions__kitten__bearcat']['preprocess functions']; + $this->assertIdentical($expected_preprocess_functions, $preprocess_functions, 'Suggestion implemented as a template correctly inherits preprocess functions.'); + + $this->assertTrue(isset($registry_theme->get()['theme_test_preprocess_suggestions__kitten__meerkat__tarsier__moose']), 'Preprocess function with an unimplemented lower-level suggestion is added to the registry.'); + } + + /** + * Tests that the theme registry can be altered by themes. + */ + public function testThemeRegistryAlterByTheme() { + + /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */ + $theme_handler = \Drupal::service('theme_handler'); + $theme_handler->install(['test_theme']); + $theme_handler->setDefault('test_theme'); + + $registry = new Registry(\Drupal::root(), \Drupal::cache(), \Drupal::lock(), \Drupal::moduleHandler(), $theme_handler, \Drupal::service('theme.initialization'), 'test_theme'); + $registry->setThemeManager(\Drupal::theme()); + $this->assertEqual('value', $registry->get()['theme_test_template_test']['variables']['additional']); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php new file mode 100644 index 0000000..a71c3fe --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/StableLibraryOverrideTest.php @@ -0,0 +1,186 @@ +container->get('theme_installer')->install(['stable']); + + // Enable all core modules. + $all_modules = system_rebuild_module_data(); + $all_modules = array_filter($all_modules, function ($module) { + // Filter contrib, hidden, experimental, already enabled modules, and + // modules in the Testing package. + if ($module->origin !== 'core' || !empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing' || $module->info['package'] == 'Core (Experimental)') { + return FALSE; + } + return TRUE; + }); + $this->allModules = array_keys($all_modules); + $this->allModules[] = 'system'; + sort($this->allModules); + $this->container->get('module_installer')->install($this->allModules); + + $this->themeManager = $this->container->get('theme.manager'); + $this->themeInitialization = $this->container->get('theme.initialization'); + $this->libraryDiscovery = $this->container->get('library.discovery'); + } + + /** + * Ensures that Stable overrides all relevant core library assets. + */ + public function testStableLibraryOverrides() { + // First get the clean library definitions with no active theme. + $libraries_before = $this->getAllLibraries(); + $libraries_before = $this->removeVendorAssets($libraries_before); + + $this->themeManager->setActiveTheme($this->themeInitialization->getActiveThemeByName('stable')); + $this->libraryDiscovery->clearCachedDefinitions(); + + // Now get the library definitions with Stable as the active theme. + $libraries_after = $this->getAllLibraries(); + $libraries_after = $this->removeVendorAssets($libraries_after); + + $root = \Drupal::root(); + foreach ($libraries_before as $extension => $libraries) { + foreach ($libraries as $library_name => $library) { + // Allow skipping libraries. + if (in_array("$extension/$library_name", $this->librariesToSkip)) { + continue; + } + $library_after = $libraries_after[$extension][$library_name]; + + // Check that all the CSS assets are overridden. + foreach ($library['css'] as $index => $asset) { + $clean_path = $asset['data']; + $stable_path = $library_after['css'][$index]['data']; + // Make core/misc assets look like they are coming from a "core" + // module. + $replacements = [ + 'core/misc/' => "core/modules/core/css/", + ]; + $expected_path = strtr($clean_path, $replacements); + + // Adjust the module asset paths to correspond with the Stable folder + // structure. + $expected_path = str_replace("core/modules/$extension/css/", "core/themes/stable/css/$extension/", $expected_path); + $assert_path = str_replace("core/modules/$extension/", '', $clean_path); + + $this->assertEqual($expected_path, $stable_path, "$assert_path from the $extension/$library_name library is overridden in Stable."); + } + } + } + } + + /** + * Removes all vendor libraries and assets from the library definitions. + * + * @param array[] $all_libraries + * An associative array of libraries keyed by extension, then by library + * name, and so on. + * + * @return array[] + * The reduced array of libraries. + */ + protected function removeVendorAssets($all_libraries) { + foreach ($all_libraries as $extension => $libraries) { + foreach ($libraries as $library_name => $library) { + if (isset($library['remote'])) { + unset($all_libraries[$extension][$library_name]); + } + foreach (['css', 'js'] as $asset_type) { + foreach ($library[$asset_type] as $index => $asset) { + if (strpos($asset['data'], 'core/assets/vendor') !== FALSE) { + unset($all_libraries[$extension][$library_name][$asset_type][$index]); + // Re-key the array of assets. This is needed because + // libraries-override doesn't always preserve the order. + if (!empty($all_libraries[$extension][$library_name][$asset_type])) { + $all_libraries[$extension][$library_name][$asset_type] = array_values($all_libraries[$extension][$library_name][$asset_type]); + } + } + } + } + } + } + return $all_libraries; + } + + /** + * Gets all libraries for core and all installed modules. + * + * @return array[] + * An associative array of libraries keyed by extension, then by library + * name, and so on. + */ + protected function getAllLibraries() { + $modules = \Drupal::moduleHandler()->getModuleList(); + $module_list = array_keys($modules); + sort($module_list); + $this->assertEqual($this->allModules, $module_list, 'All core modules are installed.'); + + $libraries['core'] = $this->libraryDiscovery->getLibrariesByExtension('core'); + + $root = \Drupal::root(); + foreach ($modules as $module_name => $module) { + $library_file = $module->getPath() . '/' . $module_name . '.libraries.yml'; + if (is_file($root . '/' . $library_file)) { + $libraries[$module_name] = $this->libraryDiscovery->getLibrariesByExtension($module_name); + } + } + return $libraries; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/StableThemeTest.php b/core/tests/Drupal/KernelTests/Core/Theme/StableThemeTest.php new file mode 100644 index 0000000..da585d0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/StableThemeTest.php @@ -0,0 +1,70 @@ +themeHandler = $this->container->get('theme_handler'); + $this->themeManager = $this->container->get('theme.manager'); + } + + /** + * Ensures Stable is used by default when no base theme has been defined. + */ + public function testStableIsDefault() { + $this->themeHandler->install(['test_stable']); + $this->themeHandler->setDefault('test_stable'); + $theme = $this->themeManager->getActiveTheme(); + /** @var \Drupal\Core\Theme\ActiveTheme $base_theme */ + $base_themes = $theme->getBaseThemes(); + $base_theme = reset($base_themes); + $this->assertTrue($base_theme->getName() == 'stable', "Stable theme is the base theme if a theme hasn't decided to opt out."); + } + + /** + * Tests opting out of Stable by setting the base theme to false. + */ + public function testWildWest() { + $this->themeHandler->install(['test_wild_west']); + $this->themeHandler->setDefault('test_wild_west'); + $theme = $this->themeManager->getActiveTheme(); + /** @var \Drupal\Core\Theme\ActiveTheme $base_theme */ + $base_themes = $theme->getBaseThemes(); + $this->assertTrue(empty($base_themes), 'No base theme is set when a theme has opted out of using Stable.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php new file mode 100644 index 0000000..b82978c --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeInstallerTest.php @@ -0,0 +1,398 @@ +register('router.dumper', 'Drupal\Core\Routing\NullMatcherDumper'); + } + + protected function setUp() { + parent::setUp(); + $this->installConfig(array('system')); + } + + /** + * Verifies that no themes are installed by default. + */ + function testEmpty() { + $this->assertFalse($this->extensionConfig()->get('theme')); + + $this->assertFalse(array_keys($this->themeHandler()->listInfo())); + $this->assertFalse(array_keys(system_list('theme'))); + + // Rebuilding available themes should always yield results though. + $this->assertTrue($this->themeHandler()->rebuildThemeData()['stark'], 'ThemeHandler::rebuildThemeData() yields all available themes.'); + + // theme_get_setting() should return global default theme settings. + $this->assertIdentical(theme_get_setting('features.favicon'), TRUE); + } + + /** + * Tests installing a theme. + */ + function testInstall() { + $name = 'test_basetheme'; + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(isset($themes[$name])); + + $this->themeInstaller()->install(array($name)); + + $this->assertIdentical($this->extensionConfig()->get("theme.$name"), 0); + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertEqual($themes[$name]->getName(), $name); + + $this->assertEqual(array_keys(system_list('theme')), array_keys($themes)); + + // Verify that test_basetheme.settings is active. + $this->assertIdentical(theme_get_setting('features.favicon', $name), FALSE); + $this->assertEqual(theme_get_setting('base', $name), 'only'); + $this->assertEqual(theme_get_setting('override', $name), 'base'); + } + + /** + * Tests installing a sub-theme. + */ + function testInstallSubTheme() { + $name = 'test_subtheme'; + $base_name = 'test_basetheme'; + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(array_keys($themes)); + + $this->themeInstaller()->install(array($name)); + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertTrue(isset($themes[$base_name])); + + $this->themeInstaller()->uninstall(array($name)); + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(isset($themes[$name])); + $this->assertTrue(isset($themes[$base_name])); + } + + /** + * Tests installing a non-existing theme. + */ + function testInstallNonExisting() { + $name = 'non_existing_theme'; + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(array_keys($themes)); + + try { + $message = 'ThemeHandler::install() throws InvalidArgumentException upon installing a non-existing theme.'; + $this->themeInstaller()->install(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(array_keys($themes)); + } + + /** + * Tests installing a theme with a too long name. + */ + function testInstallNameTooLong() { + $name = 'test_theme_having_veery_long_name_which_is_too_long'; + + try { + $message = 'ThemeHandler::install() throws ExtensionNameLengthException upon installing a theme with a too long name.'; + $this->themeInstaller()->install(array($name)); + $this->fail($message); + } + catch (ExtensionNameLengthException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + } + + /** + * Tests uninstalling the default theme. + */ + function testUninstallDefault() { + $name = 'stark'; + $other_name = 'bartik'; + $this->themeInstaller()->install(array($name, $other_name)); + $this->themeHandler()->setDefault($name); + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertTrue(isset($themes[$other_name])); + + try { + $message = 'ThemeHandler::uninstall() throws InvalidArgumentException upon disabling default theme.'; + $this->themeHandler()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertTrue(isset($themes[$other_name])); + } + + /** + * Tests uninstalling the admin theme. + */ + function testUninstallAdmin() { + $name = 'stark'; + $other_name = 'bartik'; + $this->themeInstaller()->install(array($name, $other_name)); + $this->config('system.theme')->set('admin', $name)->save(); + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertTrue(isset($themes[$other_name])); + + try { + $message = 'ThemeHandler::uninstall() throws InvalidArgumentException upon disabling admin theme.'; + $this->themeHandler()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertTrue(isset($themes[$other_name])); + } + + /** + * Tests uninstalling a sub-theme. + */ + function testUninstallSubTheme() { + $name = 'test_subtheme'; + $base_name = 'test_basetheme'; + + $this->themeInstaller()->install(array($name)); + $this->themeInstaller()->uninstall(array($name)); + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(isset($themes[$name])); + $this->assertTrue(isset($themes[$base_name])); + } + + /** + * Tests uninstalling a base theme before its sub-theme. + */ + function testUninstallBaseBeforeSubTheme() { + $name = 'test_basetheme'; + $sub_name = 'test_subtheme'; + + $this->themeInstaller()->install(array($sub_name)); + + try { + $message = 'ThemeHandler::install() throws InvalidArgumentException upon uninstalling base theme before sub theme.'; + $this->themeInstaller()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertTrue(isset($themes[$sub_name])); + + // Verify that uninstalling both at the same time works. + $this->themeInstaller()->uninstall(array($name, $sub_name)); + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(isset($themes[$name])); + $this->assertFalse(isset($themes[$sub_name])); + } + + /** + * Tests uninstalling a non-existing theme. + */ + function testUninstallNonExisting() { + $name = 'non_existing_theme'; + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(array_keys($themes)); + + try { + $message = 'ThemeHandler::uninstall() throws InvalidArgumentException upon uninstalling a non-existing theme.'; + $this->themeInstaller()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(array_keys($themes)); + } + + /** + * Tests uninstalling a theme. + */ + function testUninstall() { + $name = 'test_basetheme'; + + $this->themeInstaller()->install(array($name)); + $this->assertTrue($this->config("$name.settings")->get()); + + $this->themeInstaller()->uninstall(array($name)); + + $this->assertFalse(array_keys($this->themeHandler()->listInfo())); + $this->assertFalse(array_keys(system_list('theme'))); + + $this->assertFalse($this->config("$name.settings")->get()); + + // Ensure that the uninstalled theme can be installed again. + $this->themeInstaller()->install(array($name)); + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name])); + $this->assertEqual($themes[$name]->getName(), $name); + $this->assertEqual(array_keys(system_list('theme')), array_keys($themes)); + $this->assertTrue($this->config("$name.settings")->get()); + } + + /** + * Tests uninstalling a theme that is not installed. + */ + function testUninstallNotInstalled() { + $name = 'test_basetheme'; + + try { + $message = 'ThemeHandler::uninstall() throws InvalidArgumentException upon uninstalling a theme that is not installed.'; + $this->themeInstaller()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + } + + /** + * Tests that theme info can be altered by a module. + * + * @see module_test_system_info_alter() + */ + function testThemeInfoAlter() { + $name = 'seven'; + $this->container->get('state')->set('module_test.hook_system_info_alter', TRUE); + + $this->themeInstaller()->install(array($name)); + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(isset($themes[$name]->info['regions']['test_region'])); + + // Rebuild module data so we know where module_test is located. + // @todo Remove as part of https://www.drupal.org/node/2186491 + system_rebuild_module_data(); + $this->moduleInstaller()->install(array('module_test'), FALSE); + $this->assertTrue($this->moduleHandler()->moduleExists('module_test')); + + $themes = $this->themeHandler()->listInfo(); + $this->assertTrue(isset($themes[$name]->info['regions']['test_region'])); + + // Legacy assertions. + // @todo Remove once theme initialization/info has been modernized. + // @see https://www.drupal.org/node/2228093 + $info = system_get_info('theme', $name); + $this->assertTrue(isset($info['regions']['test_region'])); + $regions = system_region_list($name); + $this->assertTrue(isset($regions['test_region'])); + $system_list = system_list('theme'); + $this->assertTrue(isset($system_list[$name]->info['regions']['test_region'])); + + $this->moduleInstaller()->uninstall(array('module_test')); + $this->assertFalse($this->moduleHandler()->moduleExists('module_test')); + + $themes = $this->themeHandler()->listInfo(); + $this->assertFalse(isset($themes[$name]->info['regions']['test_region'])); + + // Legacy assertions. + // @todo Remove once theme initialization/info has been modernized. + // @see https://www.drupal.org/node/2228093 + $info = system_get_info('theme', $name); + $this->assertFalse(isset($info['regions']['test_region'])); + $regions = system_region_list($name); + $this->assertFalse(isset($regions['test_region'])); + $system_list = system_list('theme'); + $this->assertFalse(isset($system_list[$name]->info['regions']['test_region'])); + } + + /** + * Returns the theme handler service. + * + * @return \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected function themeHandler() { + return $this->container->get('theme_handler'); + } + + /** + * Returns the theme installer service. + * + * @return \Drupal\Core\Extension\ThemeInstallerInterface + */ + protected function themeInstaller() { + return $this->container->get('theme_installer'); + } + + /** + * Returns the system.theme config object. + * + * @return \Drupal\Core\Config\Config + */ + protected function extensionConfig() { + return $this->config('core.extension'); + } + + /** + * Returns the ModuleHandler. + * + * @return \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected function moduleHandler() { + return $this->container->get('module_handler'); + } + + /** + * Returns the ModuleInstaller. + * + * @return \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected function moduleInstaller() { + return $this->container->get('module_installer'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php b/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php new file mode 100644 index 0000000..6c2dfb3 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/ThemeSettingsTest.php @@ -0,0 +1,63 @@ +installConfig(array('system')); + + if (!isset($this->availableThemes)) { + $discovery = new ExtensionDiscovery(\Drupal::root()); + $this->availableThemes = $discovery->scan('theme'); + } + } + + /** + * Tests that $theme.settings are imported and used as default theme settings. + */ + function testDefaultConfig() { + $name = 'test_basetheme'; + $path = $this->availableThemes[$name]->getPath(); + $this->assertTrue(file_exists("$path/" . InstallStorage::CONFIG_INSTALL_DIRECTORY . "/$name.settings.yml")); + $this->container->get('theme_handler')->install(array($name)); + $this->assertIdentical(theme_get_setting('base', $name), 'only'); + } + + /** + * Tests that the $theme.settings default config file is optional. + */ + function testNoDefaultConfig() { + $name = 'stark'; + $path = $this->availableThemes[$name]->getPath(); + $this->assertFalse(file_exists("$path/" . InstallStorage::CONFIG_INSTALL_DIRECTORY . "/$name.settings.yml")); + $this->container->get('theme_handler')->install(array($name)); + $this->assertNotNull(theme_get_setting('features.favicon', $name)); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php b/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php new file mode 100644 index 0000000..deaf033 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/TwigEnvironmentTest.php @@ -0,0 +1,130 @@ +container->get('renderer'); + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + $environment = \Drupal::service('twig'); + $this->assertEqual($environment->renderInline('test-no-context'), 'test-no-context'); + $this->assertEqual($environment->renderInline('test-with-context {{ llama }}', array('llama' => 'muuh')), 'test-with-context muuh'); + + $element = array(); + $unsafe_string = ''; + $element['test'] = array( + '#type' => 'inline_template', + '#template' => 'test-with-context {{ unsafe_content }}', + '#context' => array('unsafe_content' => $unsafe_string), + ); + $this->assertEqual($renderer->renderRoot($element), 'test-with-context ' . Html::escape($unsafe_string) . ''); + + // Enable twig_auto_reload and twig_debug. + $settings = Settings::getAll(); + $settings['twig_debug'] = TRUE; + $settings['twig_auto_reload'] = TRUE; + + new Settings($settings); + $this->container = \Drupal::service('kernel')->rebuildContainer(); + \Drupal::setContainer($this->container); + + $element = array(); + $element['test'] = array( + '#type' => 'inline_template', + '#template' => 'test-with-context {{ llama }}', + '#context' => array('llama' => 'muuh'), + ); + $element_copy = $element; + // Render it twice so that twig caching is triggered. + $this->assertEqual($renderer->renderRoot($element), 'test-with-context muuh'); + $this->assertEqual($renderer->renderRoot($element_copy), 'test-with-context muuh'); + + // Tests caching of inline templates with long content to ensure the + // generated cache key can be used as a filename. + $element = []; + $element['test'] = [ + '#type' => 'inline_template', + '#template' => 'Llamas sometimes spit and wrestle with their {{ llama }}. Kittens are soft and fuzzy and they sometimes say {{ kitten }}. Flamingos have long legs and they are usually {{ flamingo }}. Pandas eat bamboo and they are {{ panda }}. Giraffes have long necks and long tongues and they eat {{ giraffe }}.', + '#context' => [ + 'llama' => 'necks', + 'kitten' => 'meow', + 'flamingo' => 'pink', + 'panda' => 'bears', + 'giraffe' => 'leaves', + ], + ]; + $expected = 'Llamas sometimes spit and wrestle with their necks. Kittens are soft and fuzzy and they sometimes say meow. Flamingos have long legs and they are usually pink. Pandas eat bamboo and they are bears. Giraffes have long necks and long tongues and they eat leaves.'; + $element_copy = $element; + + // Render it twice so that twig caching is triggered. + $this->assertEqual($renderer->renderRoot($element), $expected); + $this->assertEqual($renderer->renderRoot($element_copy), $expected); + + $name = '{# inline_template_start #}' . $element['test']['#template']; + $hash = $this->container->getParameter('twig_extension_hash'); + + $cache = $environment->getCache(); + $class = $environment->getTemplateClass($name); + $expected = $hash . '_inline-template' . '_' . hash('sha256', $class); + $this->assertEqual($expected, $cache->generateKey($name, $class)); + } + + /** + * Tests that exceptions are thrown when a template is not found. + */ + public function testTemplateNotFoundException() { + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + $environment = \Drupal::service('twig'); + + try { + $environment->loadTemplate('this-template-does-not-exist.html.twig')->render(array()); + $this->fail('Did not throw an exception as expected.'); + } + catch (\Twig_Error_Loader $e) { + $this->assertTrue(strpos($e->getMessage(), 'Template "this-template-does-not-exist.html.twig" is not defined') === 0); + } + } + + /** + * Ensures that cacheFilename() varies by extensions + deployment identifier. + */ + public function testCacheFilename() { + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + // Note: Later we refetch the twig service in order to bypass its internal + // static cache. + $environment = \Drupal::service('twig'); + + $original_filename = $environment->getCacheFilename('core/modules/system/templates/container.html.twig'); + \Drupal::getContainer()->set('twig', NULL); + + \Drupal::service('module_installer')->install(['twig_extension_test']); + $environment = \Drupal::service('twig'); + $new_extension_filename = $environment->getCacheFilename('core/modules/system/templates/container.html.twig'); + \Drupal::getContainer()->set('twig', NULL); + + $this->assertNotEqual($new_extension_filename, $original_filename); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php b/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php index 3ac7a5f..2e6e5a4 100644 --- a/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php +++ b/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php @@ -45,7 +45,11 @@ public function testMarkupInterfaceEmpty($expected, $variable) { */ public function providerTestMarkupInterfaceEmpty() { return [ + // @codingStandardsIgnoreStart + // The first argument to \Drupal\Core\StringTranslation\TranslatableMarkup + // is not supposed to be an empty string. 'empty TranslatableMarkup' => ['', new TranslatableMarkup('')], + // @codingStandardsIgnoreEnd 'non-empty TranslatableMarkup' => ['test', new TranslatableMarkup('test')], 'empty FormattableMarkup' => ['', new FormattableMarkup('', ['@foo' => 'bar'])], 'non-empty FormattableMarkup' => ['bar', new FormattableMarkup('@foo', ['@foo' => 'bar'])], diff --git a/core/tests/Drupal/KernelTests/Core/Theme/TwigWhiteListTest.php b/core/tests/Drupal/KernelTests/Core/Theme/TwigWhiteListTest.php new file mode 100644 index 0000000..41984d6 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/TwigWhiteListTest.php @@ -0,0 +1,126 @@ +installSchema('system', array('sequences')); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('taxonomy_term'); + NodeType::create([ + 'type' => 'page', + 'name' => 'Basic page', + 'display_submitted' => FALSE, + ])->save(); + // Add a vocabulary so we can test different view modes. + $vocabulary = Vocabulary::create([ + 'name' => $this->randomMachineName(), + 'description' => $this->randomMachineName(), + 'vid' => $this->randomMachineName(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'help' => '', + ]); + $vocabulary->save(); + + // Add a term to the vocabulary. + $this->term = Term::create([ + 'name' => 'Sometimes people are just jerks', + 'description' => $this->randomMachineName(), + 'vid' => $vocabulary->id(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + $this->term->save(); + + // Create a field. + $handler_settings = array( + 'target_bundles' => array( + $vocabulary->id() => $vocabulary->id(), + ), + 'auto_create' => TRUE, + ); + // Add the term field. + FieldStorageConfig::create(array( + 'field_name' => 'field_term', + 'type' => 'entity_reference', + 'entity_type' => 'node', + 'cardinality' => 1, + 'settings' => array( + 'target_type' => 'taxonomy_term', + ), + ))->save(); + FieldConfig::create(array( + 'field_name' => 'field_term', + 'entity_type' => 'node', + 'bundle' => 'page', + 'label' => 'Terms', + 'settings' => array( + 'handler' => 'default', + 'handler_settings' => $handler_settings, + ), + ))->save(); + + // Show on default display and teaser. + entity_get_display('node', 'page', 'default') + ->setComponent('field_term', array( + 'type' => 'entity_reference_label', + )) + ->save(); + // Boot twig environment. + $this->twig = \Drupal::service('twig'); + } + + /** + * Tests white-listing of methods doesn't interfere with chaining. + */ + public function testWhiteListChaining() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'Some node mmk', + 'status' => 1, + 'field_term' => $this->term->id(), + ]); + $node->save(); + $this->setRawContent(twig_render_template(drupal_get_path('theme', 'test_theme') . '/templates/node.html.twig', ['node' => $node])); + $this->assertText('Sometimes people are just jerks'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/AllowedValuesConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/AllowedValuesConstraintValidatorTest.php new file mode 100644 index 0000000..4a23fbb --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/TypedData/AllowedValuesConstraintValidatorTest.php @@ -0,0 +1,54 @@ +typedData = $this->container->get('typed_data_manager'); + } + + /** + * Tests the AllowedValues validation constraint validator. + * + * For testing we define an integer with a set of allowed values. + */ + public function testValidation() { + // Create a definition that specifies some AllowedValues. + $definition = DataDefinition::create('integer') + ->addConstraint('AllowedValues', array(1, 2, 3)); + + // Test the validation. + $typed_data = $this->typedData->create($definition, 1); + $violations = $typed_data->validate(); + $this->assertEqual($violations->count(), 0, 'Validation passed for correct value.'); + + // Test the validation when an invalid value is passed. + $typed_data = $this->typedData->create($definition, 4); + $violations = $typed_data->validate(); + $this->assertEqual($violations->count(), 1, 'Validation failed for incorrect value.'); + + // Make sure the information provided by a violation is correct. + $violation = $violations[0]; + $this->assertEqual($violation->getMessage(), t('The value you selected is not a valid choice.'), 'The message for invalid value is correct.'); + $this->assertEqual($violation->getRoot(), $typed_data, 'Violation root is correct.'); + $this->assertEqual($violation->getInvalidValue(), 4, 'The invalid value is set correctly in the violation.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/ComplexDataConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/ComplexDataConstraintValidatorTest.php new file mode 100644 index 0000000..078cb7a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/TypedData/ComplexDataConstraintValidatorTest.php @@ -0,0 +1,79 @@ +typedData = $this->container->get('typed_data_manager'); + } + + /** + * Tests the ComplexData validation constraint validator. + * + * For testing a map including a constraint on one of its keys is defined. + */ + public function testValidation() { + // Create a definition that specifies some ComplexData constraint. + $definition = MapDataDefinition::create() + ->setPropertyDefinition('key', DataDefinition::create('integer')) + ->addConstraint('ComplexData', array( + 'key' => array( + 'AllowedValues' => array(1, 2, 3) + ), + )); + + // Test the validation. + $typed_data = $this->typedData->create($definition, array('key' => 1)); + $violations = $typed_data->validate(); + $this->assertEqual($violations->count(), 0, 'Validation passed for correct value.'); + + // Test the validation when an invalid value is passed. + $typed_data = $this->typedData->create($definition, array('key' => 4)); + $violations = $typed_data->validate(); + $this->assertEqual($violations->count(), 1, 'Validation failed for incorrect value.'); + + // Make sure the information provided by a violation is correct. + $violation = $violations[0]; + $this->assertEqual($violation->getMessage(), t('The value you selected is not a valid choice.'), 'The message for invalid value is correct.'); + $this->assertEqual($violation->getRoot(), $typed_data, 'Violation root is correct.'); + $this->assertEqual($violation->getInvalidValue(), 4, 'The invalid value is set correctly in the violation.'); + + // Test using the constraint with a map without the specified key. This + // should be ignored as long as there is no NotNull or NotBlank constraint. + $typed_data = $this->typedData->create($definition, array('foo' => 'bar')); + $violations = $typed_data->validate(); + $this->assertEqual($violations->count(), 0, 'Constraint on non-existing key is ignored.'); + + $definition = MapDataDefinition::create() + ->setPropertyDefinition('key', DataDefinition::create('integer')) + ->addConstraint('ComplexData', array( + 'key' => array( + 'NotNull' => array() + ), + )); + + $typed_data = $this->typedData->create($definition, array('foo' => 'bar')); + $violations = $typed_data->validate(); + $this->assertEqual($violations->count(), 1, 'Key is required.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php new file mode 100644 index 0000000..4ff9ff6 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataDefinitionTest.php @@ -0,0 +1,99 @@ +typedDataManager = $this->container->get('typed_data_manager'); + } + + /** + * Tests deriving metadata about list items. + */ + public function testLists() { + $list_definition = ListDataDefinition::create('string'); + $this->assertTrue($list_definition instanceof ListDataDefinitionInterface); + $item_definition = $list_definition->getItemDefinition(); + $this->assertTrue($item_definition instanceof DataDefinitionInterface); + $this->assertEqual($item_definition->getDataType(), 'string'); + + // Test using the definition factory. + $list_definition2 = $this->typedDataManager->createListDataDefinition('string'); + $this->assertTrue($list_definition2 instanceof ListDataDefinitionInterface); + $this->assertEqual($list_definition, $list_definition2); + + // Test creating the definition of data with type 'list', which is the same + // as creating a definition of a list of items of type 'any'. + $list_definition = $this->typedDataManager->createDataDefinition('list'); + $this->assertTrue($list_definition instanceof ListDataDefinitionInterface); + $this->assertEqual($list_definition->getDataType(), 'list'); + $this->assertEqual($list_definition->getItemDefinition()->getDataType(), 'any'); + } + + /** + * Tests deriving metadata about maps. + */ + public function testMaps() { + $map_definition = MapDataDefinition::create() + ->setPropertyDefinition('one', DataDefinition::create('string')) + ->setPropertyDefinition('two', DataDefinition::create('string')) + ->setPropertyDefinition('three', DataDefinition::create('string')); + + $this->assertTrue($map_definition instanceof ComplexDataDefinitionInterface); + + // Test retrieving metadata about contained properties. + $this->assertEqual(array_keys($map_definition->getPropertyDefinitions()), array('one', 'two', 'three')); + $this->assertEqual($map_definition->getPropertyDefinition('one')->getDataType(), 'string'); + $this->assertNull($map_definition->getMainPropertyName()); + $this->assertNull($map_definition->getPropertyDefinition('invalid')); + + // Test using the definition factory. + $map_definition2 = $this->typedDataManager->createDataDefinition('map'); + $this->assertTrue($map_definition2 instanceof ComplexDataDefinitionInterface); + $map_definition2->setPropertyDefinition('one', DataDefinition::create('string')) + ->setPropertyDefinition('two', DataDefinition::create('string')) + ->setPropertyDefinition('three', DataDefinition::create('string')); + $this->assertEqual($map_definition, $map_definition2); + } + + /** + * Tests deriving metadata from data references. + */ + public function testDataReferences() { + $language_reference_definition = DataReferenceDefinition::create('language'); + $this->assertTrue($language_reference_definition instanceof DataReferenceDefinitionInterface); + + // Test retrieving metadata about the referenced data. + $this->assertEqual($language_reference_definition->getTargetDefinition()->getDataType(), 'language'); + + // Test using the definition factory. + $language_reference_definition2 = $this->typedDataManager->createDataDefinition('language_reference'); + $this->assertTrue($language_reference_definition2 instanceof DataReferenceDefinitionInterface); + $this->assertEqual($language_reference_definition, $language_reference_definition2); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php new file mode 100644 index 0000000..08f27b3 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php @@ -0,0 +1,634 @@ +installEntitySchema('file'); + $this->typedDataManager = $this->container->get('typed_data_manager'); + } + + /** + * Creates a typed data object and ensures it implements TypedDataInterface. + * + * @see \Drupal\Core\TypedData\TypedDataManager::create() + */ + protected function createTypedData($definition, $value = NULL, $name = NULL) { + if (is_array($definition)) { + $definition = DataDefinition::create($definition['type']); + } + $data = $this->typedDataManager->create($definition, $value, $name); + $this->assertTrue($data instanceof TypedDataInterface, 'Typed data object is an instance of the typed data interface.'); + return $data; + } + + /** + * Tests the basics around constructing and working with typed data objects. + */ + public function testGetAndSet() { + // Boolean type. + $typed_data = $this->createTypedData(array('type' => 'boolean'), TRUE); + $this->assertTrue($typed_data instanceof BooleanInterface, 'Typed data object is an instance of BooleanInterface.'); + $this->assertTrue($typed_data->getValue() === TRUE, 'Boolean value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(FALSE); + $this->assertTrue($typed_data->getValue() === FALSE, 'Boolean value was changed.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $this->assertTrue(is_string($typed_data->getString()), 'Boolean value was converted to string'); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Boolean wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // String type. + $value = $this->randomString(); + $typed_data = $this->createTypedData(array('type' => 'string'), $value); + $this->assertTrue($typed_data instanceof StringInterface, 'Typed data object is an instance of StringInterface.'); + $this->assertTrue($typed_data->getValue() === $value, 'String value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $new_value = $this->randomString(); + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'String value was changed.'); + $this->assertEqual($typed_data->validate()->count(), 0); + // Funky test. + $this->assertTrue(is_string($typed_data->getString()), 'String value was converted to string'); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'String wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(array('no string')); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // Integer type. + $value = rand(); + $typed_data = $this->createTypedData(array('type' => 'integer'), $value); + $this->assertTrue($typed_data instanceof IntegerInterface, 'Typed data object is an instance of IntegerInterface.'); + $this->assertTrue($typed_data->getValue() === $value, 'Integer value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $new_value = rand(); + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'Integer value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Integer value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Integer wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // Float type. + $value = 123.45; + $typed_data = $this->createTypedData(array('type' => 'float'), $value); + $this->assertTrue($typed_data instanceof FloatInterface, 'Typed data object is an instance of FloatInterface.'); + $this->assertTrue($typed_data->getValue() === $value, 'Float value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $new_value = 678.90; + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'Float value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Float value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Float wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // Date Time type. + $value = '2014-01-01T20:00:00+00:00'; + $typed_data = $this->createTypedData(array('type' => 'datetime_iso8601'), $value); + $this->assertTrue($typed_data instanceof DateTimeInterface, 'Typed data object is an instance of DateTimeInterface.'); + $this->assertTrue($typed_data->getValue() == $value, 'Date value was fetched.'); + $this->assertEqual($typed_data->getValue(), $typed_data->getDateTime()->format('c'), 'Value representation of a date is ISO 8601'); + $this->assertEqual($typed_data->validate()->count(), 0); + $new_value = '2014-01-02T20:00:00+00:00'; + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getDateTime()->format('c') === $new_value, 'Date value was changed and set by an ISO8601 date.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $this->assertTrue($typed_data->getDateTime()->format('Y-m-d') == '2014-01-02', 'Date value was changed and set by date string.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getDateTime(), 'Date wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + // Check implementation of DateTimeInterface. + $typed_data = $this->createTypedData(array('type' => 'datetime_iso8601'), '2014-01-01T20:00:00+00:00'); + $this->assertTrue($typed_data->getDateTime() instanceof DrupalDateTime); + $typed_data->setDateTime(new DrupalDateTime('2014-01-02T20:00:00+00:00')); + $this->assertEqual($typed_data->getValue(), '2014-01-02T20:00:00+00:00'); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getDateTime()); + + // Timestamp type. + $value = REQUEST_TIME; + $typed_data = $this->createTypedData(array('type' => 'timestamp'), $value); + $this->assertTrue($typed_data instanceof DateTimeInterface, 'Typed data object is an instance of DateTimeInterface.'); + $this->assertTrue($typed_data->getValue() == $value, 'Timestamp value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $new_value = REQUEST_TIME + 1; + $typed_data->setValue($new_value); + $this->assertTrue($typed_data->getValue() === $new_value, 'Timestamp value was changed and set.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getDateTime(), 'Timestamp wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + // Check implementation of DateTimeInterface. + $typed_data = $this->createTypedData(array('type' => 'timestamp'), REQUEST_TIME); + $this->assertTrue($typed_data->getDateTime() instanceof DrupalDateTime); + $typed_data->setDateTime(DrupalDateTime::createFromTimestamp(REQUEST_TIME + 1)); + $this->assertEqual($typed_data->getValue(), REQUEST_TIME + 1); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getDateTime()); + + // DurationIso8601 type. + $value = 'PT20S'; + $typed_data = $this->createTypedData(array('type' => 'duration_iso8601'), $value); + $this->assertTrue($typed_data instanceof DurationInterface, 'Typed data object is an instance of DurationInterface.'); + $this->assertIdentical($typed_data->getValue(), $value, 'DurationIso8601 value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('P40D'); + $this->assertEqual($typed_data->getDuration()->d, 40, 'DurationIso8601 value was changed and set by duration string.'); + $this->assertTrue(is_string($typed_data->getString()), 'DurationIso8601 value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'DurationIso8601 wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + // Check implementation of DurationInterface. + $typed_data = $this->createTypedData(array('type' => 'duration_iso8601'), 'PT20S'); + $this->assertTrue($typed_data->getDuration() instanceof \DateInterval); + $typed_data->setDuration(new \DateInterval('P40D')); + // @todo: Should we make this "nicer"? + $this->assertEqual($typed_data->getValue(), 'P0Y0M40DT0H0M0S'); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getDuration()); + + // Time span type. + $value = 20; + $typed_data = $this->createTypedData(array('type' => 'timespan'), $value); + $this->assertTrue($typed_data instanceof DurationInterface, 'Typed data object is an instance of DurationInterface.'); + $this->assertIdentical($typed_data->getValue(), $value, 'Time span value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(60 * 60 * 4); + $this->assertEqual($typed_data->getDuration()->s, 14400, 'Time span was changed'); + $this->assertTrue(is_string($typed_data->getString()), 'Time span value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Time span wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + // Check implementation of DurationInterface. + $typed_data = $this->createTypedData(array('type' => 'timespan'), 20); + $this->assertTrue($typed_data->getDuration() instanceof \DateInterval); + $typed_data->setDuration(new \DateInterval('PT4H')); + $this->assertEqual($typed_data->getValue(), 60 * 60 * 4); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getDuration()); + + // URI type. + $uri = 'http://example.com/foo/'; + $typed_data = $this->createTypedData(array('type' => 'uri'), $uri); + $this->assertTrue($typed_data instanceof UriInterface, 'Typed data object is an instance of UriInterface.'); + $this->assertTrue($typed_data->getValue() === $uri, 'URI value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue($uri . 'bar.txt'); + $this->assertTrue($typed_data->getValue() === $uri . 'bar.txt', 'URI value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'URI value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'URI wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + $typed_data->setValue('public://field/image/Photo on 4-28-14 at 12.01 PM.jpg'); + $this->assertEqual($typed_data->validate()->count(), 0, 'Filename with spaces is valid.'); + + // Generate some files that will be used to test the binary data type. + $files = array(); + for ($i = 0; $i < 3; $i++) { + $path = "public://example_$i.png"; + file_unmanaged_copy(\Drupal::root() . '/core/misc/druplicon.png', $path); + $image = File::create(['uri' => $path]); + $image->save(); + $files[] = $image; + } + + // Email type. + $value = $this->randomString(); + $typed_data = $this->createTypedData(array('type' => 'email'), $value); + $this->assertTrue($typed_data instanceof StringInterface, 'Typed data object is an instance of StringInterface.'); + $this->assertIdentical($typed_data->getValue(), $value, 'Email value was fetched.'); + $new_value = 'test@example.com'; + $typed_data->setValue($new_value); + $this->assertIdentical($typed_data->getValue(), $new_value, 'Email value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Email value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Email wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalidATexample.com'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // Binary type. + $typed_data = $this->createTypedData(array('type' => 'binary'), $files[0]->getFileUri()); + $this->assertTrue($typed_data instanceof BinaryInterface, 'Typed data object is an instance of BinaryInterface.'); + $this->assertTrue(is_resource($typed_data->getValue()), 'Binary value was fetched.'); + $this->assertEqual($typed_data->validate()->count(), 0); + // Try setting by URI. + $typed_data->setValue($files[1]->getFileUri()); + $this->assertEqual(fgets($typed_data->getValue()), fgets(fopen($files[1]->getFileUri(), 'r')), 'Binary value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Binary value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + // Try setting by resource. + $typed_data->setValue(fopen($files[2]->getFileUri(), 'r')); + $this->assertEqual(fgets($typed_data->getValue()), fgets(fopen($files[2]->getFileUri(), 'r')), 'Binary value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Binary value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Binary wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue('invalid'); + $this->assertEqual($typed_data->validate()->count(), 1, 'Validation detected invalid value.'); + + // Any type. + $value = array('foo'); + $typed_data = $this->createTypedData(array('type' => 'any'), $value); + $this->assertIdentical($typed_data->getValue(), $value, 'Any value was fetched.'); + $new_value = 'test@example.com'; + $typed_data->setValue($new_value); + $this->assertIdentical($typed_data->getValue(), $new_value, 'Any value was changed.'); + $this->assertTrue(is_string($typed_data->getString()), 'Any value was converted to string'); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue(), 'Any wrapper is null-able.'); + $this->assertEqual($typed_data->validate()->count(), 0); + // We cannot test invalid values as everything is valid for the any type, + // but make sure an array or object value passes validation also. + $typed_data->setValue(array('entry')); + $this->assertEqual($typed_data->validate()->count(), 0); + $typed_data->setValue((object) array('entry')); + $this->assertEqual($typed_data->validate()->count(), 0); + } + + /** + * Tests using typed data lists. + */ + public function testTypedDataLists() { + // Test working with an existing list of strings. + $value = array('one', 'two', 'three'); + $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); + $this->assertEqual($typed_data->getValue(), $value, 'List value has been set.'); + // Test iterating. + $count = 0; + foreach ($typed_data as $item) { + $this->assertTrue($item instanceof TypedDataInterface); + $count++; + } + $this->assertEqual($count, 3); + + // Test getting the string representation. + $this->assertEqual($typed_data->getString(), 'one, two, three'); + $typed_data[1] = ''; + $this->assertEqual($typed_data->getString(), 'one, three'); + + // Test using array access. + $this->assertEqual($typed_data[0]->getValue(), 'one'); + $typed_data[] = 'four'; + $this->assertEqual($typed_data[3]->getValue(), 'four'); + $this->assertEqual($typed_data->count(), 4); + $this->assertTrue(isset($typed_data[0])); + $this->assertTrue(!isset($typed_data[6])); + + // Test isEmpty and cloning. + $this->assertFalse($typed_data->isEmpty()); + $clone = clone $typed_data; + $this->assertTrue($typed_data->getValue() === $clone->getValue()); + $this->assertTrue($typed_data[0] !== $clone[0]); + $clone->setValue(array()); + $this->assertTrue($clone->isEmpty()); + + // Make sure that resetting the value using NULL results in an empty array. + $clone->setValue(array()); + $typed_data->setValue(NULL); + $this->assertIdentical($typed_data->getValue(), array()); + $this->assertIdentical($clone->getValue(), array()); + + // Test dealing with NULL items. + $typed_data[] = NULL; + $this->assertTrue($typed_data->isEmpty()); + $this->assertEqual(count($typed_data), 1); + $typed_data[] = ''; + $this->assertFalse($typed_data->isEmpty()); + $this->assertEqual(count($typed_data), 2); + $typed_data[] = 'three'; + $this->assertFalse($typed_data->isEmpty()); + $this->assertEqual(count($typed_data), 3); + + $this->assertEqual($typed_data->getValue(), array(NULL, '', 'three')); + // Test unsetting. + unset($typed_data[1]); + $this->assertEqual(count($typed_data), 2); + // Check that items were shifted. + $this->assertEqual($typed_data[1]->getValue(), 'three'); + + // Getting a not set list item returns NULL, and does not create a new item. + $this->assertNull($typed_data[2]); + $this->assertEqual(count($typed_data), 2); + + // Test setting the list with less values. + $typed_data->setValue(array('one')); + $this->assertEqual($typed_data->count(), 1); + + // Test setting invalid values. + try { + $typed_data->setValue('string'); + $this->fail('No exception has been thrown when setting an invalid value.'); + } + catch (\Exception $e) { + $this->pass('Exception thrown:' . $e->getMessage()); + } + } + + /** + * Tests the filter() method on typed data lists. + */ + public function testTypedDataListsFilter() { + // Check that an all-pass filter leaves the list untouched. + $value = array('zero', 'one'); + $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); + $typed_data->filter(function(TypedDataInterface $item) { + return TRUE; + }); + $this->assertEqual($typed_data->count(), 2); + $this->assertEqual($typed_data[0]->getValue(), 'zero'); + $this->assertEqual($typed_data[0]->getName(), 0); + $this->assertEqual($typed_data[1]->getValue(), 'one'); + $this->assertEqual($typed_data[1]->getName(), 1); + + // Check that a none-pass filter empties the list. + $value = array('zero', 'one'); + $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); + $typed_data->filter(function(TypedDataInterface $item) { + return FALSE; + }); + $this->assertEqual($typed_data->count(), 0); + + // Check that filtering correctly renumbers elements. + $value = array('zero', 'one', 'two'); + $typed_data = $this->createTypedData(ListDataDefinition::create('string'), $value); + $typed_data->filter(function(TypedDataInterface $item) { + return $item->getValue() !== 'one'; + }); + $this->assertEqual($typed_data->count(), 2); + $this->assertEqual($typed_data[0]->getValue(), 'zero'); + $this->assertEqual($typed_data[0]->getName(), 0); + $this->assertEqual($typed_data[1]->getValue(), 'two'); + $this->assertEqual($typed_data[1]->getName(), 1); + } + + /** + * Tests using a typed data map. + */ + public function testTypedDataMaps() { + // Test working with a simple map. + $value = array( + 'one' => 'eins', + 'two' => 'zwei', + 'three' => 'drei', + ); + $definition = MapDataDefinition::create() + ->setPropertyDefinition('one', DataDefinition::create('string')) + ->setPropertyDefinition('two', DataDefinition::create('string')) + ->setPropertyDefinition('three', DataDefinition::create('string')); + + $typed_data = $this->createTypedData($definition, $value); + + // Test iterating. + $count = 0; + foreach ($typed_data as $item) { + $this->assertTrue($item instanceof TypedDataInterface); + $count++; + } + $this->assertEqual($count, 3); + + // Test retrieving metadata. + $this->assertEqual(array_keys($typed_data->getDataDefinition()->getPropertyDefinitions()), array_keys($value)); + $definition = $typed_data->getDataDefinition()->getPropertyDefinition('one'); + $this->assertEqual($definition->getDataType(), 'string'); + $this->assertNull($typed_data->getDataDefinition()->getPropertyDefinition('invalid')); + + // Test getting and setting properties. + $this->assertEqual($typed_data->get('one')->getValue(), 'eins'); + $this->assertEqual($typed_data->toArray(), $value); + $typed_data->set('one', 'uno'); + $this->assertEqual($typed_data->get('one')->getValue(), 'uno'); + // Make sure the update is reflected in the value of the map also. + $value = $typed_data->getValue(); + $this->assertEqual($value, array( + 'one' => 'uno', + 'two' => 'zwei', + 'three' => 'drei' + )); + + $properties = $typed_data->getProperties(); + $this->assertEqual(array_keys($properties), array_keys($value)); + $this->assertIdentical($properties['one'], $typed_data->get('one'), 'Properties are identical.'); + + // Test setting a not defined property. It shouldn't show up in the + // properties, but be kept in the values. + $typed_data->setValue(array('foo' => 'bar')); + $this->assertEqual(array_keys($typed_data->getProperties()), array('one', 'two', 'three')); + $this->assertEqual(array_keys($typed_data->getValue()), array('foo', 'one', 'two', 'three')); + + // Test getting the string representation. + $typed_data->setValue(array('one' => 'eins', 'two' => '', 'three' => 'drei')); + $this->assertEqual($typed_data->getString(), 'eins, drei'); + + // Test isEmpty and cloning. + $this->assertFalse($typed_data->isEmpty()); + $clone = clone $typed_data; + $this->assertTrue($typed_data->getValue() === $clone->getValue()); + $this->assertTrue($typed_data->get('one') !== $clone->get('one')); + $clone->setValue(array()); + $this->assertTrue($clone->isEmpty()); + + // Make sure the difference between NULL (not set) and an empty array is + // kept. + $typed_data->setValue(NULL); + $this->assertNull($typed_data->getValue()); + $typed_data->setValue(array()); + $value = $typed_data->getValue(); + $this->assertTrue(isset($value) && is_array($value)); + + // Test accessing invalid properties. + $typed_data->setValue($value); + try { + $typed_data->get('invalid'); + $this->fail('No exception has been thrown when getting an invalid value.'); + } + catch (\Exception $e) { + $this->pass('Exception thrown:' . $e->getMessage()); + } + + // Test setting invalid values. + try { + $typed_data->setValue('invalid'); + $this->fail('No exception has been thrown when setting an invalid value.'); + } + catch (\Exception $e) { + $this->pass('Exception thrown:' . $e->getMessage()); + } + + // Test adding a new property to the map. + $typed_data->getDataDefinition()->setPropertyDefinition('zero', DataDefinition::create('any')); + $typed_data->set('zero', 'null'); + $this->assertEqual($typed_data->get('zero')->getValue(), 'null'); + $definition = $typed_data->get('zero')->getDataDefinition(); + $this->assertEqual($definition->getDataType(), 'any', 'Definition for a new map entry returned.'); + } + + /** + * Tests typed data validation. + */ + public function testTypedDataValidation() { + $definition = DataDefinition::create('integer') + ->setConstraints(array( + 'Range' => array('min' => 5), + )); + $violations = $this->typedDataManager->create($definition, 10)->validate(); + $this->assertEqual($violations->count(), 0); + + $integer = $this->typedDataManager->create($definition, 1); + $violations = $integer->validate(); + $this->assertEqual($violations->count(), 1); + + // Test translating violation messages. + $message = t('This value should be %limit or more.', array('%limit' => 5)); + $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); + $this->assertEqual($violations[0]->getPropertyPath(), ''); + $this->assertIdentical($violations[0]->getRoot(), $integer, 'Root object returned.'); + + // Test translating violation messages when pluralization is used. + $definition = DataDefinition::create('string') + ->setConstraints(array( + 'Length' => array('min' => 10), + )); + $violations = $this->typedDataManager->create($definition, "short")->validate(); + $this->assertEqual($violations->count(), 1); + $message = t('This value is too short. It should have %limit characters or more.', array('%limit' => 10)); + $this->assertEqual($violations[0]->getMessage(), $message, 'Translated violation message retrieved.'); + + // Test having multiple violations. + $definition = DataDefinition::create('integer') + ->setConstraints(array( + 'Range' => array('min' => 5), + 'Null' => array(), + )); + $violations = $this->typedDataManager->create($definition, 10)->validate(); + $this->assertEqual($violations->count(), 1); + $violations = $this->typedDataManager->create($definition, 1)->validate(); + $this->assertEqual($violations->count(), 2); + + // Test validating property containers and make sure the NotNull and Null + // constraints work with typed data containers. + $definition = BaseFieldDefinition::create('integer') + ->setConstraints(array('NotNull' => array())); + $field_item = $this->typedDataManager->create($definition, array('value' => 10)); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 0); + + $field_item = $this->typedDataManager->create($definition, array('value' => 'no integer')); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + $this->assertEqual($violations[0]->getPropertyPath(), '0.value'); + + // Test that the field item may not be empty. + $field_item = $this->typedDataManager->create($definition); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + + // Test the Null constraint with typed data containers. + $definition = BaseFieldDefinition::create('float') + ->setConstraints(array('Null' => array())); + $field_item = $this->typedDataManager->create($definition, array('value' => 11.5)); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 1); + $field_item = $this->typedDataManager->create($definition); + $violations = $field_item->validate(); + $this->assertEqual($violations->count(), 0); + + // Test getting constraint definitions by type. + $definitions = $this->typedDataManager->getValidationConstraintManager()->getDefinitionsByType('entity'); + $this->assertTrue(isset($definitions['EntityType']), 'Constraint plugin found for type entity.'); + $this->assertTrue(isset($definitions['Null']), 'Constraint plugin found for type entity.'); + $this->assertTrue(isset($definitions['NotNull']), 'Constraint plugin found for type entity.'); + + $definitions = $this->typedDataManager->getValidationConstraintManager()->getDefinitionsByType('string'); + $this->assertFalse(isset($definitions['EntityType']), 'Constraint plugin not found for type string.'); + $this->assertTrue(isset($definitions['Null']), 'Constraint plugin found for type string.'); + $this->assertTrue(isset($definitions['NotNull']), 'Constraint plugin found for type string.'); + + // Test automatic 'required' validation. + $definition = DataDefinition::create('integer') + ->setRequired(TRUE); + $violations = $this->typedDataManager->create($definition)->validate(); + $this->assertEqual($violations->count(), 1); + $violations = $this->typedDataManager->create($definition, 0)->validate(); + $this->assertEqual($violations->count(), 0); + + // Test validating a list of a values and make sure property paths starting + // with "0" are created. + $definition = BaseFieldDefinition::create('integer'); + $violations = $this->typedDataManager->create($definition, array(array('value' => 10)))->validate(); + $this->assertEqual($violations->count(), 0); + $violations = $this->typedDataManager->create($definition, array(array('value' => 'string')))->validate(); + $this->assertEqual($violations->count(), 1); + + $this->assertEqual($violations[0]->getInvalidValue(), 'string'); + $this->assertIdentical($violations[0]->getPropertyPath(), '0.value'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php b/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php new file mode 100644 index 0000000..ee1b6e1 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Update/CompatibilityFixTest.php @@ -0,0 +1,49 @@ +getEditable('core.extension'); + + // Add an incompatible/non-existent module to the config. + $modules = $extension_config->get('module'); + $modules['incompatible_module'] = 0; + $extension_config->set('module', $modules); + $modules = $extension_config->get('module'); + $this->assertTrue(in_array('incompatible_module', array_keys($modules)), 'Added incompatible/non-existent module to the config.'); + + // Add an incompatible/non-existent theme to the config. + $themes = $extension_config->get('theme'); + $themes['incompatible_theme'] = 0; + $extension_config->set('theme', $themes); + $themes = $extension_config->get('theme'); + $this->assertTrue(in_array('incompatible_theme', array_keys($themes)), 'Added incompatible/non-existent theme to the config.'); + + // Fix compatibility. + update_fix_compatibility(); + $modules = $extension_config->get('module'); + $this->assertFalse(in_array('incompatible_module', array_keys($modules)), 'Fixed modules compatibility.'); + $themes = $extension_config->get('theme'); + $this->assertFalse(in_array('incompatible_theme', array_keys($themes)), 'Fixed themes compatibility.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php b/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php new file mode 100644 index 0000000..185a3fe --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Updater/UpdaterTest.php @@ -0,0 +1,32 @@ +assertEqual('module handler test multiple', $title); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Url/LinkGenerationTest.php b/core/tests/Drupal/KernelTests/Core/Url/LinkGenerationTest.php new file mode 100644 index 0000000..3d7ce5e --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Url/LinkGenerationTest.php @@ -0,0 +1,65 @@ +executeInRenderContext(new RenderContext(), function () use ($url) { + return \Drupal::l(['#markup' => 'link with markup'], $url); + }); + $this->setRawContent($link); + $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); + // Ensure the content of the link is not escaped. + $this->assertRaw('link with markup'); + + // Test just adding text to an already safe string. + \Drupal::state()->set('link_generation_test_link_alter', TRUE); + $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) { + return \Drupal::l(['#markup' => 'link with markup'], $url); + }); + $this->setRawContent($link); + $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); + // Ensure the content of the link is escaped. + $this->assertEscaped('link with markup Test!'); + + // Test passing a safe string to t(). + \Drupal::state()->set('link_generation_test_link_alter_safe', TRUE); + $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) { + return \Drupal::l(['#markup' => 'link with markup'], $url); + }); + $this->setRawContent($link); + $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); + // Ensure the content of the link is escaped. + $this->assertRaw('link with markup Test!'); + + // Test passing an unsafe string to t(). + $link = $renderer->executeInRenderContext(new RenderContext(), function () use ($url) { + return \Drupal::l('link with markup', $url); + }); + $this->setRawContent($link); + $this->assertTrue($link instanceof MarkupInterface, 'The output of link generation is marked safe as it is a link.'); + // Ensure the content of the link is escaped. + $this->assertEscaped('link with markup'); + $this->assertRaw('Test!'); + } + +} diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index ff01d0c..8c88165 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -261,18 +261,6 @@ protected function bootEnvironment() { $this->databasePrefix = 'simpletest' . $suffix; } while (is_dir($this->root . '/' . $this->siteDirectory)); - $this->vfsRoot = vfsStream::setup('root', NULL, array( - 'sites' => array( - 'simpletest' => array( - $suffix => array(), - ), - ), - )); - $this->siteDirectory = vfsStream::url('root/sites/simpletest/' . $suffix); - - mkdir($this->siteDirectory . '/files', 0775); - mkdir($this->siteDirectory . '/files/config/' . CONFIG_SYNC_DIRECTORY, 0775, TRUE); - // Ensure that all code that relies on drupal_valid_test_ua() can still be // safely executed. This primarily affects the (test) site directory // resolution (used by e.g. LocalStream and PhpStorage). @@ -288,9 +276,7 @@ protected function bootEnvironment() { ); new Settings($settings); - $GLOBALS['config_directories'] = array( - CONFIG_SYNC_DIRECTORY => $this->siteDirectory . '/files/config/sync', - ); + $this->setUpFilesystem(); foreach (Database::getAllConnectionInfo() as $key => $targets) { Database::removeConnection($key); @@ -299,6 +285,32 @@ protected function bootEnvironment() { } /** + * Sets up the filesystem, so things like the file directory. + */ + protected function setUpFilesystem() { + $suffix = str_replace('simpletest', '', $this->databasePrefix); + $this->vfsRoot = vfsStream::setup('root', NULL, array( + 'sites' => array( + 'simpletest' => array( + $suffix => array(), + ), + ), + )); + $this->siteDirectory = vfsStream::url('root/sites/simpletest/' . $suffix); + + mkdir($this->siteDirectory . '/files', 0775); + mkdir($this->siteDirectory . '/files/config/' . CONFIG_SYNC_DIRECTORY, 0775, TRUE); + + $settings = Settings::getInstance() ? Settings::getAll() : []; + $settings['file_public_path'] = $this->siteDirectory . '/files'; + new Settings($settings); + + $GLOBALS['config_directories'] = array( + CONFIG_SYNC_DIRECTORY => $this->siteDirectory . '/files/config/sync', + ); + } + + /** * @return string */ public function getDatabasePrefix() { @@ -663,7 +675,7 @@ protected function assertPostConditions() { } // Shut down the kernel (if bootKernel() was called). - // @see \Drupal\system\Tests\DrupalKernel\DrupalKernelTest + // @see \Drupal\KernelTests\Core\DrupalKernel\DrupalKernelTest if ($this->container) { $this->container->get('kernel')->shutdown(); } @@ -998,7 +1010,7 @@ protected function render(array &$elements) { * \Drupal\Core\Site\Settings::get() to perform custom merges. */ protected function setSetting($name, $value) { - $settings = Settings::getAll(); + $settings = Settings::getInstance() ? Settings::getAll() : []; $settings[$name] = $value; new Settings($settings); } diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index c304f59..5a9b0c0 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -752,7 +752,7 @@ protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NUL // Grant the specified permissions to the role, if any. if (!empty($permissions)) { user_role_grant_permissions($role->id(), $permissions); - $assigned_permissions = entity_load('user_role', $role->id())->getPermissions(); + $assigned_permissions = Role::load($role->id())->getPermissions(); $missing_permissions = array_diff($permissions, $assigned_permissions); if ($missing_permissions) { $this->fail(SafeMarkup::format('Failed to create permissions: @perms', array('@perms' => implode(', ', $missing_permissions)))); diff --git a/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php index dfd12bb..7bc2382 100644 --- a/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php +++ b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php @@ -172,10 +172,24 @@ public function testRemoveService() $this->assertTrue($otherService->preFooInvoked); } - public function testGetListenerPriority() + public function testGetListenerPriorityWithServices() { - // Override the parent test as our implementation doesn't define - // getListenerPriority(). + $container = new ContainerBuilder(); + $container->register('listener_service', TestEventListener::class); + + $listeners = array( + 'test_event' => array( + 5 => array( + array('service' => array('listener_service', 'preFoo')), + ), + ), + ); + + $dispatcher = new ContainerAwareEventDispatcher($container, $listeners); + $listenerService = $container->get('listener_service'); + $actualPriority = $dispatcher->getListenerPriority('test_event', [$listenerService, 'preFoo']); + + $this->assertSame(5, $actualPriority); } } diff --git a/core/tests/Drupal/Tests/Core/Controller/ControllerResolverTest.php b/core/tests/Drupal/Tests/Core/Controller/ControllerResolverTest.php index fec4d4c..98a18a3 100644 --- a/core/tests/Drupal/Tests/Core/Controller/ControllerResolverTest.php +++ b/core/tests/Drupal/Tests/Core/Controller/ControllerResolverTest.php @@ -14,7 +14,8 @@ use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Tests\UnitTestCase; -use Symfony\Component\DependencyInjection\ContainerAware; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -280,7 +281,8 @@ public function getResult() { } } -class MockContainerAware extends ContainerAware { +class MockContainerAware implements ContainerAwareInterface { + use ContainerAwareTrait; public function getResult() { return 'This is container aware.'; } diff --git a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php index 2bf2639..883f851 100644 --- a/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php +++ b/core/tests/Drupal/Tests/Core/Database/Driver/pgsql/PostgresqlConnectionTest.php @@ -40,6 +40,11 @@ public function providerEscapeTables() { array('"camelCase"', 'camelCase'), array('"camelCase"', '"camelCase"'), array('"camelCase"', 'camel/Case'), + // Sometimes, table names are following the pattern database.schema.table. + array('"camelCase".nocase.nocase', 'camelCase.nocase.nocase'), + array('nocase."camelCase".nocase', 'nocase.camelCase.nocase'), + array('nocase.nocase."camelCase"', 'nocase.nocase.camelCase'), + array('"camelCase"."camelCase"."camelCase"', 'camelCase.camelCase.camelCase'), ); } diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php new file mode 100644 index 0000000..1a9dc66 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php @@ -0,0 +1,43 @@ + $yml, + ]); + + $builder = new ContainerBuilder(); + $yaml_file_loader = new YamlFileLoader($builder); + $yaml_file_loader->load('vfs://drupal/modules/example/example.yml'); + + $this->assertEquals(['_provider' => [['provider' => 'example']]], $builder->getDefinition('example_service')->getTags()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php index c028e0a..36caa00 100644 --- a/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php +++ b/core/tests/Drupal/Tests/Core/DrupalKernel/DrupalKernelTest.php @@ -126,12 +126,12 @@ public function testFindSitePath() { ] ]]); - define('DRUPAL_ROOT', $vfs_root->url('drupal_root')); $request = new Request(); $request->server->set('SERVER_NAME', 'www.example.org'); $request->server->set('SERVER_PORT', '8888'); $request->server->set('SCRIPT_NAME', '/index.php'); - $this->assertEquals('sites/example', DrupalKernel::findSitePath($request)); + $this->assertEquals('sites/example', DrupalKernel::findSitePath($request, TRUE, $vfs_root->url('drupal_root'))); + $this->assertEquals('sites/example', DrupalKernel::findSitePath($request, FALSE, $vfs_root->url('drupal_root'))); } } diff --git a/core/tests/Drupal/Tests/Core/EventSubscriber/CustomPageExceptionHtmlSubscriberTest.php b/core/tests/Drupal/Tests/Core/EventSubscriber/CustomPageExceptionHtmlSubscriberTest.php index 5d3993d..81e7cc6 100644 --- a/core/tests/Drupal/Tests/Core/EventSubscriber/CustomPageExceptionHtmlSubscriberTest.php +++ b/core/tests/Drupal/Tests/Core/EventSubscriber/CustomPageExceptionHtmlSubscriberTest.php @@ -14,6 +14,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\RequestContext; /** * @coversDefaultClass \Drupal\Core\EventSubscriber\CustomPageExceptionHtmlSubscriber @@ -127,6 +128,12 @@ protected function tearDown() { public function testHandleWithPostRequest() { $request = Request::create('/test', 'POST', array('name' => 'druplicon', 'pass' => '12345')); + $request_context = new RequestContext(); + $request_context->fromRequest($request); + $this->accessUnawareRouter->expects($this->any()) + ->method('getContext') + ->willReturn($request_context); + $this->kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { return new HtmlResponse($request->getMethod()); })); @@ -148,6 +155,12 @@ public function testHandleWithGetRequest() { $request = Request::create('/test', 'GET', array('name' => 'druplicon', 'pass' => '12345')); $request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, AccessResult::forbidden()->addCacheTags(['druplicon'])); + $request_context = new RequestContext(); + $request_context->fromRequest($request); + $this->accessUnawareRouter->expects($this->any()) + ->method('getContext') + ->willReturn($request_context); + $this->kernel->expects($this->once())->method('handle')->will($this->returnCallback(function (Request $request) { return new Response($request->getMethod() . ' ' . UrlHelper::buildQuery($request->query->all())); })); diff --git a/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php b/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php index 2ceb9a3..d81360f 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/Discovery/DerivativeDiscoveryDecoratorTest.php @@ -111,14 +111,14 @@ public function testNonExistentDerivativeFetcher() { * @see \Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator::getDeriver().\ * * @expectedException \Drupal\Component\Plugin\Exception\InvalidDeriverException - * @expectedExceptionMessage Plugin (invalid_discovery) deriver "\Drupal\system\Tests\Plugin\DerivativeTest" must implement \Drupal\Component\Plugin\Derivative\DeriverInterface. + * @expectedExceptionMessage Plugin (invalid_discovery) deriver "\Drupal\KernelTests\Core\Plugin\DerivativeTest" must implement \Drupal\Component\Plugin\Derivative\DeriverInterface. */ public function testInvalidDerivativeFetcher() { $definitions = array(); // Do this with a class that doesn't implement the interface. $definitions['invalid_discovery'] = array( 'id' => 'invalid_discovery', - 'deriver' => '\Drupal\system\Tests\Plugin\DerivativeTest', + 'deriver' => '\Drupal\KernelTests\Core\Plugin\DerivativeTest', ); $this->discoveryMain->expects($this->any()) ->method('getDefinitions') diff --git a/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php b/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php index 593ce60..0f5142c 100644 --- a/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php +++ b/core/tests/Drupal/Tests/Core/StackMiddleware/NegotiationMiddlewareTest.php @@ -46,7 +46,7 @@ protected function setUp() { */ public function testAjaxIframeUpload() { $request = new Request(); - $request->attributes->set('ajax_iframe_upload', '1'); + $request->request->set('ajax_iframe_upload', '1'); $this->assertSame('iframeupload', $this->contentNegotiation->getContentType($request)); } @@ -101,9 +101,11 @@ public function testHandle() { $request->setRequestFormat('html')->shouldBeCalled(); // Some getContentType calls we don't really care about but have to mock. - $request->get('ajax_iframe_upload', FALSE)->shouldBeCalled(); + $request_data = $this->prophesize(ParameterBag::class); + $request_data->get('ajax_iframe_upload', FALSE)->shouldBeCalled(); $request_mock = $request->reveal(); $request_mock->query = new ParameterBag([]); + $request_mock->request = $request_data->reveal(); // Calling kernel app with default arguments. $this->app->handle($request_mock, HttpKernelInterface::MASTER_REQUEST, TRUE) @@ -126,9 +128,11 @@ public function testSetFormat() { // Some calls we don't care about. $request->setRequestFormat('html')->shouldBeCalled(); - $request->get('ajax_iframe_upload', FALSE)->shouldBeCalled(); + $request_data = $this->prophesize(ParameterBag::class); + $request_data->get('ajax_iframe_upload', FALSE)->shouldBeCalled(); $request_mock = $request->reveal(); $request_mock->query = new ParameterBag([]); + $request_mock->request = $request_data->reveal(); // Trigger handle. $this->contentNegotiation->registerFormat('david', 'geeky/david'); diff --git a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php index 97fb9d3..1299b3f 100644 --- a/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Utility/LinkGeneratorTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\Core\Utility { use Drupal\Component\Render\MarkupInterface; +use Drupal\Core\GeneratedNoLink; use Drupal\Core\GeneratedUrl; use Drupal\Core\Language\Language; use Drupal\Core\Link; @@ -147,6 +148,26 @@ public function testGenerate() { } /** + * Tests the generate() method with the route. + * + * @covers ::generate + */ + public function testGenerateNoLink() { + $this->urlGenerator->expects($this->never()) + ->method('generateFromRoute'); + $this->moduleHandler->expects($this->once()) + ->method('alter') + ->with('link', $this->isType('array')); + + $url = Url::fromRoute(''); + $url->setUrlGenerator($this->urlGenerator); + + $result = $this->linkGenerator->generate('Test', $url); + $this->assertTrue($result instanceof GeneratedNoLink); + $this->assertSame('Test', (string) $result); + } + + /** * Tests the generate() method with an external URL. * * The set_active_class option is set to TRUE to ensure this does not cause diff --git a/core/themes/bartik/css/components/buttons.css b/core/themes/bartik/css/components/buttons.css index e0eb43b..823315d 100644 --- a/core/themes/bartik/css/components/buttons.css +++ b/core/themes/bartik/css/components/buttons.css @@ -19,6 +19,8 @@ text-align: center; padding: 0.250em 1.063em; border-radius: 1em; + display: inline-block; + line-height: normal; } .button:hover, .button:active, diff --git a/core/themes/bartik/css/components/dropbutton.component.css b/core/themes/bartik/css/components/dropbutton.component.css index db55965..b5113f5 100644 --- a/core/themes/bartik/css/components/dropbutton.component.css +++ b/core/themes/bartik/css/components/dropbutton.component.css @@ -3,10 +3,6 @@ * Visual styles for Bartik's dropbutton component. */ -.js .dropbutton-wrapper .dropbutton-widget { - /* This is required to win over specifity of .js td .dropbutton-widget */ - position: relative; -} .js .dropbutton-widget { border: 1px solid; border-color: #e4e4e4 #d2d2d2 #b4b4b4 #d2d2d2; diff --git a/core/themes/bartik/css/components/feed-icon.css b/core/themes/bartik/css/components/feed-icon.css index 0189eeb..7b4ac76 100644 --- a/core/themes/bartik/css/components/feed-icon.css +++ b/core/themes/bartik/css/components/feed-icon.css @@ -4,7 +4,12 @@ */ .feed-icon { - border-bottom: none; + border-bottom: 1px solid transparent; display: inline-block; - padding: 15px 0 0 0; + padding-bottom: 2px; +} + +.feed-icon:focus, +.feed-icon:hover { + border-color: #018fe2; } diff --git a/core/themes/bartik/css/components/field.css b/core/themes/bartik/css/components/field.css index 231f8d7..a2a16e0 100644 --- a/core/themes/bartik/css/components/field.css +++ b/core/themes/bartik/css/components/field.css @@ -56,7 +56,7 @@ } } .field--type-image img, -.field--name-field-user-picture img { +.field--name-user-picture img { margin: 0 0 1em; } .field--type-image a { diff --git a/core/themes/bartik/css/components/form.css b/core/themes/bartik/css/components/form.css index dd5ac0c..f8bcfcc 100644 --- a/core/themes/bartik/css/components/form.css +++ b/core/themes/bartik/css/components/form.css @@ -15,6 +15,18 @@ form { } fieldset { margin: 1em 0; + min-width: 0; +} +/** + * We've temporarily added this Firefox specific rule here to fix fieldset + * widths. + * @todo remove once this Mozilla bug is fixed. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=504622 + */ +@-moz-document url-prefix() { + fieldset { + display: table-cell; + } } details, fieldset, @@ -116,9 +128,13 @@ input.form-submit:focus { margin-right: 1.2em; margin-left: 0; } -.form-item label { +.form-item label, +.form-wrapper .label { font-size: 0.929em; } +.form-wrapper .field-multiple-table .label { + font-size: inherit; +} .form-type-radio label, .form-type-checkbox label { margin-left: 4px; /* LTR */ @@ -146,9 +162,13 @@ input.form-submit:focus { margin-bottom: 2em; } .node-form label, -.node-form .description { +.node-form .description, +.node-form .form-wrapper .label { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } +.node-form .form-wrapper .field-multiple-table .label { + font-family: inherit; +} .node-form .form-wrapper { margin-bottom: 2em; } diff --git a/core/themes/bartik/css/components/header.css b/core/themes/bartik/css/components/header.css index 793c53a..40ea0f0 100644 --- a/core/themes/bartik/css/components/header.css +++ b/core/themes/bartik/css/components/header.css @@ -27,7 +27,11 @@ float: right; } } - +@media screen and (max-width: 460px) { + .region-header { + padding-bottom: 0.357em; + } +} /* Region header blocks. */ .region-header .block:not(.site-branding) { font-size: 0.857em; diff --git a/core/themes/bartik/css/components/node.css b/core/themes/bartik/css/components/node.css index 78caa34..96dfb16 100644 --- a/core/themes/bartik/css/components/node.css +++ b/core/themes/bartik/css/components/node.css @@ -40,11 +40,11 @@ color: #68696b; margin-bottom: -5px; } -.node__meta .field--name-field-user-picture img { +.node__meta .field--name-user-picture img { float: left; /* LTR */ margin: 1px 20px 0 0; /* LTR */ } -[dir="rtl"] .node__meta .field--name-field-user-picture img { +[dir="rtl"] .node__meta .field--name-user-picture img { float: right; margin-left: 20px; margin-right: 0; diff --git a/core/themes/bartik/css/components/primary-menu.css b/core/themes/bartik/css/components/primary-menu.css index 0451b8a..1132ae2 100644 --- a/core/themes/bartik/css/components/primary-menu.css +++ b/core/themes/bartik/css/components/primary-menu.css @@ -200,3 +200,13 @@ body:not(:target) .region-primary-menu .menu-toggle-target-show:target ~ .menu . display: none; } } + +/** + * Ensures that the open mobile menu hides when the screen dimensions become + * 461px or wider. + */ +@media all and (min-width: 461px) { + body:not(:target) .region-primary-menu .menu-toggle-target-show:target ~ .menu-toggle--hide { + display: none; + } +} diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine index f4e18f5..c8f8f49 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine @@ -63,9 +63,6 @@ function twig_render_template($template_file, array $variables) { try { $output['rendered_markup'] = $twig_service->loadTemplate($template_file)->render($variables); } - catch (\Twig_Error_Loader $e) { - drupal_set_message($e->getMessage(), 'error'); - } catch (\Twig_Error_Runtime $e) { // In case there is a previous exception, re-throw the previous exception, // so that the original exception is shown, rather than diff --git a/core/themes/seven/css/components/form.css b/core/themes/seven/css/components/form.css index f42d59b..947dcbe 100644 --- a/core/themes/seven/css/components/form.css +++ b/core/themes/seven/css/components/form.css @@ -10,8 +10,20 @@ fieldset:not(.fieldgroup) { border-radius: 2px; margin: 1em 0; padding: 30px 18px 18px; + min-width: 0; position: relative; } +/** + * We've temporarily added this Firefox specific rule here to fix fieldset + * widths. + * @todo remove once this Mozilla bug is fixed. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=504622 + */ +@-moz-document url-prefix() { + fieldset:not(.fieldgroup) { + display: table-cell; + } +} fieldset:not(.fieldgroup) > legend { font-size: 1em; font-weight: bold; @@ -23,6 +35,12 @@ fieldset:not(.fieldgroup) > legend { .fieldgroup { min-width: 0; } +/** + * We've temporarily added this Firefox specific rule here to fix fieldset + * widths. + * @todo remove once this Mozilla bug is fixed. + * See https://bugzilla.mozilla.org/show_bug.cgi?id=504622 + */ @-moz-document url-prefix() { .fieldgroup { display: table-cell; @@ -91,7 +109,6 @@ label[for] { background-color: #fcf4f2; } .form-required:after { - background-image: url(../../images/required.svg); background-size: 7px 7px; width: 7px; height: 7px; diff --git a/core/themes/seven/css/components/views-ui.css b/core/themes/seven/css/components/views-ui.css index bfd4575..6849781 100644 --- a/core/themes/seven/css/components/views-ui.css +++ b/core/themes/seven/css/components/views-ui.css @@ -236,9 +236,14 @@ details.fieldset-no-legend { /* @group Rearrange filter criteria */ .views-ui-rearrange-filter-form .action-links { - margin: 0; + float: left; + margin: 0 0 1em; padding: 0; } +.views-ui-rearrange-filter-form .tabledrag-toggle-weight-wrapper { + float: right; + margin-bottom: 1em; +} .views-ui-rearrange-filter-form table { border: medium none; diff --git a/core/themes/seven/images/add.png b/core/themes/seven/images/add.png deleted file mode 100644 index 1a2faf6..0000000 --- a/core/themes/seven/images/add.png +++ /dev/null @@ -1,4 +0,0 @@ -PNG - - IHDR gIDATxڅOA ӽYژicJ%B@9[[*:'`H*Ho\)x- -u^ bZJ IENDB` \ No newline at end of file diff --git a/core/themes/seven/images/arrow-asc-active.png b/core/themes/seven/images/arrow-asc-active.png deleted file mode 100644 index 7536eee..0000000 --- a/core/themes/seven/images/arrow-asc-active.png +++ /dev/null @@ -1,3 +0,0 @@ -PNG - - IHDR 6Y tEXtSoftware Adobe ImageReadyqe<
' . Html::escape($expected_html) . '
' . Html::escape($actual_html) . '
!ts