diff --git a/composer.lock b/composer.lock index 025d5bf79..58be85866 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "8c9fdf621ce53640f24b24749e59717c", + "hash": "2be29019515c847055593ea41b88475d", "content-hash": "f38613812a285c03a1a18458384fe0b1", "packages": [ { @@ -2004,7 +2004,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/47bb3388cfeae41a38087ac8465a7d08fa92ea2e", + "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/6196fdb001faf681f92db2ae10abafb5815affde", "reference": "47bb3388cfeae41a38087ac8465a7d08fa92ea2e", "shasum": "" }, @@ -2585,6 +2585,124 @@ ], "time": "2015-08-29 16:16:56" }, + { + "name": "jcalderonzumba/gastonjs", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/jcalderonzumba/gastonjs.git", + "reference": "5e231b4df98275c404e1371fc5fadd34f6a121ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jcalderonzumba/gastonjs/zipball/5e231b4df98275c404e1371fc5fadd34f6a121ad", + "reference": "5e231b4df98275c404e1371fc5fadd34f6a121ad", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~5.0|~6.0", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.6", + "silex/silex": "~1.2", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.1" + }, + "type": "phantomjs-api", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zumba\\GastonJS\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Juan Francisco Calderón Zumba", + "email": "juanfcz@gmail.com", + "homepage": "http://github.com/jcalderonzumba" + } + ], + "description": "PhantomJS API based server for webpage automation", + "homepage": "https://github.com/jcalderonzumba/gastonjs", + "keywords": [ + "api", + "automation", + "browser", + "headless", + "phantomjs" + ], + "time": "2015-10-07 11:40:41" + }, + { + "name": "jcalderonzumba/mink-phantomjs-driver", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/jcalderonzumba/MinkPhantomJSDriver.git", + "reference": "10d7c48c9a4129463052321b52450d98983c4332" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jcalderonzumba/MinkPhantomJSDriver/zipball/10d7c48c9a4129463052321b52450d98983c4332", + "reference": "10d7c48c9a4129463052321b52450d98983c4332", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6", + "jcalderonzumba/gastonjs": "~1.0", + "php": ">=5.4", + "twig/twig": "~1.8" + }, + "require-dev": { + "phpunit/phpunit": "~4.6", + "silex/silex": "~1.2", + "symfony/css-selector": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.3" + }, + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zumba\\Mink\\Driver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Juan Francisco Calderón Zumba", + "email": "juanfcz@gmail.com", + "homepage": "http://github.com/jcalderonzumba" + } + ], + "description": "PhantomJS driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "headless", + "javascript", + "phantomjs", + "testing" + ], + "time": "2015-10-05 18:24:44" + }, { "name": "mikey179/vfsStream", "version": "v1.6.0", @@ -3672,6 +3790,8 @@ "composer/semver": 0, "behat/mink": 0, "behat/mink-goutte-driver": 0, + "jcalderonzumba/gastonjs": 20, + "jcalderonzumba/mink-phantomjs-driver": 20, "mikey179/vfsstream": 0, "phpunit/phpunit": 0, "symfony/css-selector": 0 diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 315ba2e13..5d00d6650 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -49,6 +49,7 @@ maintainer. Current subsystem maintainers for Drupal 8: Ajax system - Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia - Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos +- Tim Plunkett 'tim.plunkett' https://www.drupal.org/u/tim.plunkett Asset library system - ? diff --git a/core/UPGRADE.txt b/core/UPGRADE.txt index cde885fb9..3cb5550fe 100644 --- a/core/UPGRADE.txt +++ b/core/UPGRADE.txt @@ -64,8 +64,8 @@ following the instructions in the INTRODUCTION section at the top of this file: Enable the "Put site into maintenance mode" checkbox and save the configuration. -3. Remove all old core files and directories, except for the 'sites' directory - and any custom files you added elsewhere. +3. Remove the 'core' and 'vendor' directories. Also remove all of the files + in the top-level directory, except any that you added manually. If you made modifications to files like .htaccess, composer.json, or robots.txt you will need to re-apply them from your backup, after the new diff --git a/core/assets/vendor/ckeditor/CHANGES.md b/core/assets/vendor/ckeditor/CHANGES.md index db594dc6b..3f5c45eca 100644 --- a/core/assets/vendor/ckeditor/CHANGES.md +++ b/core/assets/vendor/ckeditor/CHANGES.md @@ -1,6 +1,33 @@ CKEditor 4 Changelog ==================== +## CKEditor 4.5.4 + +New Features: + +* [#13632](http://dev.ckeditor.com/ticket/13632): Introduce error logging mechanism. +* [#13730](http://dev.ckeditor.com/ticket/13730): Switch to the new error logging mechanism. + +Fixed Issues: + +* [#9856](http://dev.ckeditor.com/ticket/9856): Fixed: Cannot use the native context menu together with the [Div Editing Area](http://ckeditor.com/addon/divarea) plugin. Thanks to [Mark Wade](https://github.com/mark-wade)! +* [#12733](http://dev.ckeditor.com/ticket/12733): [IE9+] Fixed: Radio button `onChange` does not work. Thanks to [Iliya Kostadinov](https://github.com/iliyakostadinov)! +* [#13142](http://dev.ckeditor.com/ticket/13142): [Edge] Fixed: *Ctrl+A* and then *Backspace* result in an empty `
` element. +* [#13599](http://dev.ckeditor.com/ticket/13599): Fixed: Cross-editor drag and drop of an inline widget results in error/artifacts. +* [#13640](http://dev.ckeditor.com/ticket/13640): [IE] Fixed: Dropping a widget outside the `` element is not handled correctly. +* [#13533](http://dev.ckeditor.com/ticket/13533): Fixed: No progress during upload. +* [#13680](http://dev.ckeditor.com/ticket/13680): Fixed: The parser should allow the `` element to be a child of the `` element. +* [#11724](http://dev.ckeditor.com/ticket/11724): [Touch devices] Fixed: Drop-downs often hide right after opening them. +* [#13690](http://dev.ckeditor.com/ticket/13690): Fixed: Copying content from IE to Chrome adds an extra paragraph. +* [#13284](http://dev.ckeditor.com/ticket/13284): Fixed: Cannot drag and drop a widget if the text caret is placed just after the widget instance. +* [#13516](http://dev.ckeditor.com/ticket/13516): Fixed: CKEditor removes empty HTML5 anchors without the `name` attribute. +* [#13765](http://dev.ckeditor.com/ticket/13765): [Safari 9] Fixed: Problems with rendering samples. + +Other Changes: + +* [#11725](http://dev.ckeditor.com/ticket/11725): Marked [`CKEDITOR.env.mobile`](http://docs.ckeditor.com/#!/api/CKEDITOR.env-property-mobile) as deprecated. The reason is that it is no longer clear what "mobile" means. +* [#13737](http://dev.ckeditor.com/ticket/13737): Upgraded [Bender.js](https://github.com/benderjs/benderjs) to 0.4.1. + ## CKEditor 4.5.3 New Features: diff --git a/core/assets/vendor/ckeditor/build-config.js b/core/assets/vendor/ckeditor/build-config.js index a1e09b85d..b44b24544 100644 --- a/core/assets/vendor/ckeditor/build-config.js +++ b/core/assets/vendor/ckeditor/build-config.js @@ -5,6 +5,15 @@ * CKEditor again. Alternatively, use the "build.sh" script to build it locally. * If you do so, be sure to pass it the "-s" flag. So: "sh build.sh -s". * + * If you are developing or debugging CKEditor plugins, you may want to work + * against an unoptimized (unminified) CKEditor build. To do so, you have two + * options: + * 1. Upload build-config.js to http://ckeditor.com/builder and choose the + * "Source (Big N'Slow)" option when downloading. + * 2. Use the "build.sh" script to build it locally, with one additional flag: + * "sh build.sh -s --leave-js-unminified". + * Then, replace this directory (core/assets/vendor/ckeditor) with your build. + * * NOTE: * This file is not used by CKEditor, you may remove it. * Changing this file will not change your CKEditor configuration. @@ -35,7 +44,7 @@ var CKBUILDER_CONFIG = { 'adapters', 'config.js', 'contents.css', - 'Gruntfile.js', + 'gruntfile.js', 'styles.js', 'samples', 'skins/moono/readme.md' diff --git a/core/assets/vendor/ckeditor/ckeditor.js b/core/assets/vendor/ckeditor/ckeditor.js index 54986a8d0..41a5ec7e8 100644 --- a/core/assets/vendor/ckeditor/ckeditor.js +++ b/core/assets/vendor/ckeditor/ckeditor.js @@ -2,1040 +2,1036 @@ Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. For licensing, see LICENSE.md or http://ckeditor.com/license */ -(function(){if(!window.CKEDITOR||!window.CKEDITOR.dom)window.CKEDITOR||(window.CKEDITOR=function(){var a=/(^|.*[\\\/])ckeditor\.js(?:\?.*|;.*)?$/i,f={timestamp:"F7VE",version:"4.5.3",revision:"6c70c82",rnd:Math.floor(900*Math.random())+100,_:{pending:[],basePathSrcPattern:a},status:"unloaded",basePath:function(){var b=window.CKEDITOR_BASEPATH||"";if(!b)for(var d=document.getElementsByTagName("script"),f=0;f=0;q--)if(g[q].priority<=i){g.splice(q+1,0,e);return{removeListener:c}}g.unshift(e)}return{removeListener:c}},once:function(){var a=Array.prototype.slice.call(arguments),b=a[1];a[1]=function(a){a.removeListener();return b.apply(this,arguments)};return this.on.apply(this,a)},capture:function(){CKEDITOR.event.useCapture= -1;var a=this.on.apply(this,arguments);CKEDITOR.event.useCapture=0;return a},fire:function(){var a=0,b=function(){a=1},k=0,j=function(){k=1};return function(i,e,c){var g=f(this)[i],i=a,l=k;a=k=0;if(g){var q=g.listeners;if(q.length)for(var q=q.slice(0),n,o=0;o=0&&k.listeners.splice(j,1)}},removeAllListeners:function(){var a=f(this),b;for(b in a)delete a[b]},hasListeners:function(a){return(a=f(this)[a])&&a.listeners.length>0}}}()),CKEDITOR.editor||(CKEDITOR.editor=function(){CKEDITOR._.pending.push([this,arguments]);CKEDITOR.event.call(this)},CKEDITOR.editor.prototype.fire=function(a,f){a in{instanceReady:1,loaded:1}&&(this[a]= -true);return CKEDITOR.event.prototype.fire.call(this,a,f,this)},CKEDITOR.editor.prototype.fireOnce=function(a,f){a in{instanceReady:1,loaded:1}&&(this[a]=true);return CKEDITOR.event.prototype.fireOnce.call(this,a,f,this)},CKEDITOR.event.implementOn(CKEDITOR.editor.prototype)),CKEDITOR.env||(CKEDITOR.env=function(){var a=navigator.userAgent.toLowerCase(),f=a.match(/edge[ \/](\d+.?\d*)/),b=a.indexOf("trident/")>-1,b=!(!f&&!b),b={ie:b,edge:!!f,webkit:!b&&a.indexOf(" applewebkit/")>-1,air:a.indexOf(" adobeair/")> --1,mac:a.indexOf("macintosh")>-1,quirks:document.compatMode=="BackCompat"&&(!document.documentMode||document.documentMode<10),mobile:a.indexOf("mobile")>-1,iOS:/(ipad|iphone|ipod)/.test(a),isCustomDomain:function(){if(!this.ie)return false;var a=document.domain,b=window.location.hostname;return a!=b&&a!="["+b+"]"},secure:location.protocol=="https:"};b.gecko=navigator.product=="Gecko"&&!b.webkit&&!b.ie;if(b.webkit)a.indexOf("chrome")>-1?b.chrome=true:b.safari=true;var d=0;if(b.ie){d=f?parseFloat(f[1]): -b.quirks||!document.documentMode?parseFloat(a.match(/msie (\d+)/)[1]):document.documentMode;b.ie9Compat=d==9;b.ie8Compat=d==8;b.ie7Compat=d==7;b.ie6Compat=d<7||b.quirks}if(b.gecko)if(f=a.match(/rv:([\d\.]+)/)){f=f[1].split(".");d=f[0]*1E4+(f[1]||0)*100+(f[2]||0)*1}b.air&&(d=parseFloat(a.match(/ adobeair\/(\d+)/)[1]));b.webkit&&(d=parseFloat(a.match(/ applewebkit\/(\d+)/)[1]));b.version=d;b.isCompatible=!(b.ie&&d<7)&&!(b.gecko&&d<4E4)&&!(b.webkit&&d<534);b.hidpi=window.devicePixelRatio>=2;b.needsBrFiller= -b.gecko||b.webkit||b.ie&&d>10;b.needsNbspFiller=b.ie&&d<11;b.cssClass="cke_browser_"+(b.ie?"ie":b.gecko?"gecko":b.webkit?"webkit":"unknown");if(b.quirks)b.cssClass=b.cssClass+" cke_browser_quirks";if(b.ie)b.cssClass=b.cssClass+(" cke_browser_ie"+(b.quirks?"6 cke_browser_iequirks":b.version));if(b.air)b.cssClass=b.cssClass+" cke_browser_air";if(b.iOS)b.cssClass=b.cssClass+" cke_browser_ios";if(b.hidpi)b.cssClass=b.cssClass+" cke_hidpi";return b}()),"unloaded"==CKEDITOR.status&&function(){CKEDITOR.event.implementOn(CKEDITOR); -CKEDITOR.loadFullCore=function(){if(CKEDITOR.status!="basic_ready")CKEDITOR.loadFullCore._load=1;else{delete CKEDITOR.loadFullCore;var a=document.createElement("script");a.type="text/javascript";a.src=CKEDITOR.basePath+"ckeditor.js";document.getElementsByTagName("head")[0].appendChild(a)}};CKEDITOR.loadFullCoreTimeout=0;CKEDITOR.add=function(a){(this._.pending||(this._.pending=[])).push(a)};(function(){CKEDITOR.domReady(function(){var a=CKEDITOR.loadFullCore,f=CKEDITOR.loadFullCoreTimeout;if(a){CKEDITOR.status= -"basic_ready";a&&a._load?a():f&&setTimeout(function(){CKEDITOR.loadFullCore&&CKEDITOR.loadFullCore()},f*1E3)}})})();CKEDITOR.status="basic_loaded"}(),CKEDITOR.dom={},function(){var a=[],f=CKEDITOR.env.gecko?"-moz-":CKEDITOR.env.webkit?"-webkit-":CKEDITOR.env.ie?"-ms-":"",b=/&/g,d=/>/g,h=/",amp:"&",quot:'"',nbsp:" ",shy:"­"},e=function(c,g){return g[0]=="#"?String.fromCharCode(parseInt(g.slice(1),10)):i[g]};CKEDITOR.on("reset",function(){a= -[]});CKEDITOR.tools={arrayCompare:function(c,g){if(!c&&!g)return true;if(!c||!g||c.length!=g.length)return false;for(var a=0;a"+g+""):a.push('');return a.join("")},htmlEncode:function(c){return c===void 0||c===null?"":(""+c).replace(b,"&").replace(d,">").replace(h,"<")},htmlDecode:function(c){return c.replace(j, -e)},htmlEncodeAttr:function(c){return CKEDITOR.tools.htmlEncode(c).replace(k,""")},htmlDecodeAttr:function(c){return CKEDITOR.tools.htmlDecode(c)},transformPlainTextToHtml:function(c,g){var a=g==CKEDITOR.ENTER_BR,i=this.htmlEncode(c.replace(/\r\n/g,"\n")),i=i.replace(/\t/g,"    "),e=g==CKEDITOR.ENTER_P?"p":"div";if(!a){var b=/\n{2}/g;if(b.test(i))var d="<"+e+">",h="",i=d+i.replace(b,function(){return h+d})+h}i=i.replace(/\n/g,"
");a||(i=i.replace(RegExp("
(?=)"),function(g){return CKEDITOR.tools.repeat(g,2)}));i=i.replace(/^ | $/g," ");return i=i.replace(/(>|\s) /g,function(g,c){return c+" "}).replace(/ (?=<)/g," ")},getNextNumber:function(){var c=0;return function(){return++c}}(),getNextId:function(){return"cke_"+this.getNextNumber()},getUniqueId:function(){for(var c="e",g=0;g<8;g++)c=c+Math.floor((1+Math.random())*65536).toString(16).substring(1);return c},override:function(c,g){var a=g(c);a.prototype=c.prototype;return a},setTimeout:function(c, -g,a,i,e){e||(e=window);a||(a=e);return e.setTimeout(function(){i?c.apply(a,[].concat(i)):c.apply(a)},g||0)},trim:function(){var c=/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g;return function(g){return g.replace(c,"")}}(),ltrim:function(){var c=/^[ \t\n\r]+/g;return function(g){return g.replace(c,"")}}(),rtrim:function(){var c=/[ \t\n\r]+$/g;return function(g){return g.replace(c,"")}}(),indexOf:function(c,g){if(typeof g=="function")for(var a=0,i=c.length;a=0?c[a]:null},bind:function(c,g){return function(){return c.apply(g,arguments)}},createClass:function(c){var g=c.$,a=c.base,i=c.privates||c._,e=c.proto,c=c.statics;!g&&(g=function(){a&&this.base.apply(this,arguments)});if(i)var b=g,g=function(){var g=this._||(this._={}),c;for(c in i){var a=i[c];g[c]=typeof a=="function"?CKEDITOR.tools.bind(a,this):a}b.apply(this,arguments)};if(a){g.prototype= -this.prototypedCopy(a.prototype);g.prototype.constructor=g;g.base=a;g.baseProto=a.prototype;g.prototype.base=function(){this.base=a.prototype.base;a.apply(this,arguments);this.base=arguments.callee}}e&&this.extend(g.prototype,e,true);c&&this.extend(g,c,true);return g},addFunction:function(c,g){return a.push(function(){return c.apply(g||this,arguments)})-1},removeFunction:function(c){a[c]=null},callFunction:function(c){var g=a[c];return g&&g.apply(window,Array.prototype.slice.call(arguments,1))},cssLength:function(){var c= -/^-?\d+\.?\d*px$/,g;return function(a){g=CKEDITOR.tools.trim(a+"")+"px";return c.test(g)?g:a||""}}(),convertToPx:function(){var c;return function(g){if(!c){c=CKEDITOR.dom.element.createFromHtml('
',CKEDITOR.document);CKEDITOR.document.getBody().append(c)}if(!/%$/.test(g)){c.setStyle("width",g);return c.$.clientWidth}return g}}(),repeat:function(c,g){return Array(g+1).join(c)},tryThese:function(){for(var c, -g=0,a=arguments.length;g]*?>)|^/i,'$&\n'; + $view->displayHandlers->get('default')->overrideOption('fields', array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test_data', + 'field' => 'name', + 'relationship' => 'none', + 'alter' => array( + 'text' => $text, + ), + ), + )); + $this->executeView($view); + $this->assertEqual(Xss::filter($text), $view->style_plugin->getField(0, 'name')); + } + } diff --git a/core/modules/views/src/Tests/Handler/FieldKernelTest.php b/core/modules/views/src/Tests/Handler/FieldKernelTest.php index cbf65efc4..815b1ec02 100644 --- a/core/modules/views/src/Tests/Handler/FieldKernelTest.php +++ b/core/modules/views/src/Tests/Handler/FieldKernelTest.php @@ -39,7 +39,7 @@ class FieldKernelTest extends ViewKernelTestBase { ); /** - * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views/src/Tests/Handler/FieldWebTest.php b/core/modules/views/src/Tests/Handler/FieldWebTest.php index ddc9b50e5..2d43a101d 100644 --- a/core/modules/views/src/Tests/Handler/FieldWebTest.php +++ b/core/modules/views/src/Tests/Handler/FieldWebTest.php @@ -53,7 +53,7 @@ class FieldWebTest extends HandlerTestBase { } /** - * Overrides \Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views/src/Tests/Handler/HandlerAliasTest.php b/core/modules/views/src/Tests/Handler/HandlerAliasTest.php index fdbe9ad6a..3a229a6ad 100644 --- a/core/modules/views/src/Tests/Handler/HandlerAliasTest.php +++ b/core/modules/views/src/Tests/Handler/HandlerAliasTest.php @@ -33,7 +33,7 @@ class HandlerAliasTest extends ViewKernelTestBase { } /** - * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views/src/Tests/Handler/HandlerTest.php b/core/modules/views/src/Tests/Handler/HandlerTest.php index f084f9057..6507e2315 100644 --- a/core/modules/views/src/Tests/Handler/HandlerTest.php +++ b/core/modules/views/src/Tests/Handler/HandlerTest.php @@ -45,7 +45,7 @@ class HandlerTest extends ViewTestBase { } /** - * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php b/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php index 3f2cf2182..8032af333 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayFeedTest.php @@ -48,9 +48,13 @@ class DisplayFeedTest extends PluginTestBase { // Verify a title with HTML entities is properly escaped. $node_title = 'This "cool" & "neat" article\'s title'; - $node = $this->drupalCreateNode(array( - 'title' => $node_title - )); + $node = $this->drupalCreateNode([ + 'title' => $node_title, + 'body' => [0 => [ + 'value' => 'A paragraph', + 'format' => filter_default_format(), + ]], + ]); // Test the site name setting. $site_name = $this->randomMachineName(); @@ -60,6 +64,8 @@ class DisplayFeedTest extends PluginTestBase { $result = $this->xpath('//title'); $this->assertEqual($result[0], $site_name, 'The site title is used for the feed title.'); $this->assertEqual($result[1], $node_title, 'Node title with HTML entities displays correctly.'); + // Verify HTML is properly escaped in the description field. + $this->assertRaw('<p>A paragraph</p>'); $view = $this->container->get('entity.manager')->getStorage('view')->load('test_display_feed'); $display = &$view->getDisplay('feed_1'); @@ -101,12 +107,18 @@ class DisplayFeedTest extends PluginTestBase { // Verify a title with HTML entities is properly escaped. $node_title = 'This "cool" & "neat" article\'s title'; $this->drupalCreateNode(array( - 'title' => $node_title + 'title' => $node_title, + 'body' => [0 => [ + 'value' => 'A paragraph', + 'format' => filter_default_format(), + ]], )); $this->drupalGet('test-feed-display-fields.xml'); $result = $this->xpath('//title/a'); $this->assertEqual($result[0], $node_title, 'Node title with HTML entities displays correctly.'); + // Verify HTML is properly escaped in the description field. + $this->assertRaw('<p>A paragraph</p>'); } /** diff --git a/core/modules/views/src/Tests/Plugin/DisplayPageTest.php b/core/modules/views/src/Tests/Plugin/DisplayPageTest.php index 5ce2d3f2a..7c7593dc0 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayPageTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayPageTest.php @@ -136,6 +136,8 @@ class DisplayPageTest extends ViewKernelTestBase { $this->assertTrue(isset($tree['system.admin']->subtree['views_view:views.test_page_display_menu.page_4'])); $menu_link = $tree['system.admin']->subtree['views_view:views.test_page_display_menu.page_4']->link; $this->assertEqual($menu_link->getTitle(), 'Test child (with parent)'); + $this->assertEqual($menu_link->isExpanded(), TRUE); + $this->assertEqual($menu_link->getDescription(), 'Sample description.'); } /** diff --git a/core/modules/views/src/Tests/Plugin/FilterTest.php b/core/modules/views/src/Tests/Plugin/FilterTest.php index 721b3fa33..a463fe4d4 100644 --- a/core/modules/views/src/Tests/Plugin/FilterTest.php +++ b/core/modules/views/src/Tests/Plugin/FilterTest.php @@ -39,7 +39,7 @@ class FilterTest extends PluginTestBase { } /** - * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views/src/Tests/Plugin/PluginBaseTest.php b/core/modules/views/src/Tests/Plugin/PluginBaseTest.php index 0ef1724cf..cb794349e 100644 --- a/core/modules/views/src/Tests/Plugin/PluginBaseTest.php +++ b/core/modules/views/src/Tests/Plugin/PluginBaseTest.php @@ -43,6 +43,20 @@ class PluginBaseTest extends KernelTestBase { $this->assertIdentical($result, 'en means English'); } + /** + * Test that the token replacement in views works correctly with dots. + */ + public function testViewsTokenReplaceWithDots() { + $text = '{{ argument.first }} comes before {{ argument.second }}'; + $tokens = ['{{ argument.first }}' => 'first', '{{ argument.second }}' => 'second']; + + $result = \Drupal::service('renderer')->executeInRenderContext(new RenderContext(), function () use ($text, $tokens) { + return $this->testPluginBase->viewsTokenReplace($text, $tokens); + }); + + $this->assertIdentical($result, 'first comes before second'); + } + /** * Tests viewsTokenReplace without any twig tokens. */ diff --git a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php index 7e317f275..d57fa27b0 100644 --- a/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php +++ b/core/modules/views/src/Tests/Plugin/RelationshipJoinTestBase.php @@ -30,7 +30,7 @@ abstract class RelationshipJoinTestBase extends PluginKernelTestBase { protected $rootUser; /** - * Overrides \Drupal\views\Tests\ViewKernelTestBase::setUpFixtures(). + * {@inheritdoc} */ protected function setUpFixtures() { $this->installEntitySchema('user'); diff --git a/core/modules/views/src/Tests/Plugin/ViewsSqlExceptionTest.php b/core/modules/views/src/Tests/Plugin/ViewsSqlExceptionTest.php index aded8383e..02d581f0d 100644 --- a/core/modules/views/src/Tests/Plugin/ViewsSqlExceptionTest.php +++ b/core/modules/views/src/Tests/Plugin/ViewsSqlExceptionTest.php @@ -34,7 +34,7 @@ class ViewsSqlExceptionTest extends PluginTestBase { } /** - * Overrides Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index 2646c5616..9f5d32a88 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -1855,11 +1855,11 @@ class ViewExecutable implements \Serializable { * @param string $display_id * (Optional) The display id. ( Used only to detail an exception. ) * - * @throws \InvalidArgumentException - * Thrown when the display plugin does not have a URL to return. - * * @return \Drupal\Core\Url * The display handlers URL object. + * + * @throws \InvalidArgumentException + * Thrown when the display plugin does not have a URL to return. */ public function getUrlInfo($display_id = '') { $this->initDisplay(); diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml index 2c35726c6..26b307440 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_display_feed.yml @@ -1,8 +1,12 @@ langcode: en status: true dependencies: + config: + - core.entity_view_mode.node.teaser + - field.storage.node.body module: - node + - text - user id: test_display_feed label: test_display_feed @@ -41,6 +45,68 @@ display: plugin_id: field entity_type: node entity_field: title + body: + id: body + table: node__body + field: body + 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: text_default + settings: { } + 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 + plugin_id: field filters: status: expose: @@ -75,10 +141,21 @@ display: style: type: default title: test_display_feed + display_extenders: { } display_plugin: default display_title: Master id: default position: 0 + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + max-age: -1 + tags: + - 'config:field.storage.node.body' feed_1: display_options: displays: { } @@ -90,10 +167,20 @@ display: style: type: rss sitename_title: true + display_extenders: { } display_plugin: feed display_title: Feed id: feed_1 position: 0 + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + max-age: -1 + tags: + - 'config:field.storage.node.body' feed_2: display_options: displays: { } @@ -105,7 +192,7 @@ display: options: title_field: title link_field: title - description_field: title + description_field: body creator_field: title date_field: title guid_field_options: @@ -115,14 +202,35 @@ display: type: rss sitename_title: true display_description: '' + display_extenders: { } display_plugin: feed display_title: 'Feed with Fields' id: feed_2 position: 0 + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + max-age: -1 + tags: + - 'config:field.storage.node.body' page: display_options: path: test-feed-display + display_extenders: { } display_plugin: page - display_title: Page + display_title: 'Page' id: page position: 0 + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + max-age: -1 + tags: + - 'config:field.storage.node.body' diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_rows.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_rows.yml index 2c348415b..294e2fdcc 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_rows.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_group_rows.yml @@ -7,7 +7,6 @@ dependencies: - user config: - field.storage.node.field_views_testing_group_rows - module: id: test_group_rows label: test_group_rows module: views diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_page_display_menu.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_page_display_menu.yml index 814ad4ef5..a1eb3b560 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_page_display_menu.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_page_display_menu.yml @@ -91,10 +91,11 @@ display: type: normal title: 'Test child (with parent)' parent: system.admin - description: '' + description: 'Sample description.' menu_name: admin weight: 0 context: '0' + expanded: true defaults: title: false display_plugin: page diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_search.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_search.yml index f0264a501..c6f968f7c 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_search.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_search.yml @@ -3,6 +3,7 @@ status: true dependencies: module: - node + - search - user id: test_search label: 'Search Test' @@ -115,6 +116,9 @@ display: group_type: group admin_label: '' order: DESC + exposed: false + expose: + label: '' plugin_id: search_score title: '' header: { } diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/area/TestExample.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/area/TestExample.php index 1afb7d4d5..4efe7da95 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/area/TestExample.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/area/TestExample.php @@ -28,7 +28,7 @@ class TestExample extends AreaPluginBase { } /** - * Overrides Drupal\views\Plugin\views\area\AreaPluginBase::option_definition(). + * {@inheritdoc} */ public function defineOptions() { $options = parent::defineOptions(); @@ -39,7 +39,7 @@ class TestExample extends AreaPluginBase { } /** - * Overrides Drupal\views\Plugin\views\area\AreaPluginBase::buildOptionsForm() + * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); @@ -47,7 +47,7 @@ class TestExample extends AreaPluginBase { } /** - * Implements \Drupal\views\Plugin\views\area\AreaPluginBase::render(). + * {@inheritdoc} */ public function render($empty = FALSE) { if (!$empty || !empty($this->options['empty'])) { diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_default/ArgumentDefaultTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_default/ArgumentDefaultTest.php index 96a7175ba..0ae8fffbb 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_default/ArgumentDefaultTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/argument_default/ArgumentDefaultTest.php @@ -20,7 +20,7 @@ use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase; class ArgumentDefaultTest extends ArgumentDefaultPluginBase { /** - * Overrides Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase::defineOptions(). + * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display/DisplayTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display/DisplayTest.php index 92db1d85e..5e0709664 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display/DisplayTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display/DisplayTest.php @@ -33,15 +33,14 @@ class DisplayTest extends DisplayPluginBase { protected $usesAttachments = TRUE; /** - * Overrides \Drupal\views\Plugin\views\display\DisplayPluginBase::getType(). + * {@inheritdoc} */ public function getType() { return 'test'; } /** - * Overrides - * Drupal\views\Plugin\views\display\DisplayPluginBase::defineOptions(). + * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display_extender/DisplayExtenderTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display_extender/DisplayExtenderTest.php index 44818265c..858600b34 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display_extender/DisplayExtenderTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/display_extender/DisplayExtenderTest.php @@ -39,7 +39,7 @@ class DisplayExtenderTest extends DisplayExtenderPluginBase { } /** - * Overrides Drupal\views\Plugin\views\display\DisplayPluginBase::optionsSummary(). + * {@inheritdoc} */ public function optionsSummary(&$categories, &$options) { parent::optionsSummary($categories, $options); @@ -60,7 +60,7 @@ class DisplayExtenderTest extends DisplayExtenderPluginBase { } /** - * Overrides Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase::buildOptionsForm(). + * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { switch ($form_state->get('section')) { @@ -76,7 +76,7 @@ class DisplayExtenderTest extends DisplayExtenderPluginBase { } /** - * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::submitOptionsForm(). + * {@inheritdoc} */ public function submitOptionsForm(&$form, FormStateInterface $form_state) { parent::submitOptionsForm($form, $form_state); @@ -88,21 +88,21 @@ class DisplayExtenderTest extends DisplayExtenderPluginBase { } /** - * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::defaultableSections(). + * {@inheritdoc} */ public function defaultableSections(&$sections, $section = NULL) { $sections['test_extender_test_option'] = array('test_extender_test_option'); } /** - * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::query(). + * {@inheritdoc} */ public function query() { $this->testState['query'] = TRUE; } /** - * Overrides Drupal\views\Plugin\views\display\DisplayExtenderPluginBase::preExecute(). + * {@inheritdoc} */ public function preExecute() { $this->testState['preExecute'] = TRUE; diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php index 5da095a28..1b1ba54b8 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/field/FieldTest.php @@ -43,7 +43,7 @@ class FieldTest extends FieldPluginBase { } /** - * Overrides Drupal\views\Plugin\views\field\FieldPluginBase::addSelfTokens(). + * {@inheritdoc} */ protected function addSelfTokens(&$tokens, $item) { $tokens['{{ test_token }}'] = $this->getTestValue(); diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/filter/FilterTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/filter/FilterTest.php index aa141a33d..219b3ff49 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/filter/FilterTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/filter/FilterTest.php @@ -43,7 +43,7 @@ class FilterTest extends FilterPluginBase { } /** - * Overrides Drupal\views\Plugin\views\filter\FilterPluginBase::query(). + * {@inheritdoc} */ public function query() { // Call the parent if this option is enabled. diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/join/JoinTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/join/JoinTest.php index 7298c35f4..27f5e32c5 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/join/JoinTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/join/JoinTest.php @@ -42,7 +42,7 @@ class JoinTest extends JoinPluginBase { /** - * Overrides Drupal\views\Plugin\views\join\JoinPluginBase::buildJoin(). + * {@inheritdoc} */ public function buildJoin($select_query, $table, $view_query) { // Add an additional hardcoded condition to the query. diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php index c8bac3145..ec496fbf3 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/query/QueryTest.php @@ -29,7 +29,7 @@ class QueryTest extends QueryPluginBase { protected $orderBy = array(); /** - * Implements \Drupal\views\Plugin\views\query\QueryPluginBase::defineOptions(). + * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); @@ -39,7 +39,7 @@ class QueryTest extends QueryPluginBase { } /** - * Implements \Drupal\views\Plugin\views\query\QueryPluginBase::buildOptionsForm(). + * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); @@ -98,7 +98,7 @@ class QueryTest extends QueryPluginBase { } /** - * Implements Drupal\views\Plugin\views\query\QueryPluginBase::execute(). + * {@inheritdoc} */ public function execute(ViewExecutable $view) { $result = array(); diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/row/RowTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/row/RowTest.php index 64a0041d3..19d76e6d5 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/row/RowTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/row/RowTest.php @@ -33,7 +33,7 @@ class RowTest extends RowPluginBase { public $output; /** - * Overrides Drupal\views\Plugin\views\row\RowPluginBase::defineOptions(). + * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); @@ -43,7 +43,7 @@ class RowTest extends RowPluginBase { } /** - * Overrides Drupal\views\Plugin\views\row\RowPluginBase::buildOptionsForm(). + * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); @@ -76,7 +76,7 @@ class RowTest extends RowPluginBase { } /** - * Overrides Drupal\views\Plugin\views\row\RowPluginBase::render() + * {@inheritdoc} */ public function render($row) { return $this->getOutput(); diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/MappingTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/MappingTest.php index 076ca5b9e..3bbe6f72b 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/MappingTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/MappingTest.php @@ -26,7 +26,7 @@ use Drupal\views\Plugin\views\field\NumericField; class MappingTest extends Mapping { /** - * Overrides Drupal\views\Plugin\views\style\Mapping::defineMapping(). + * {@inheritdoc} */ protected function defineMapping() { return array( diff --git a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/StyleTest.php b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/StyleTest.php index 4323f51a4..304c010fd 100644 --- a/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/StyleTest.php +++ b/core/modules/views/tests/modules/views_test_data/src/Plugin/views/style/StyleTest.php @@ -41,7 +41,7 @@ class StyleTest extends StylePluginBase { protected $usesRowPlugin = TRUE; /** - * Overrides Drupal\views\Plugin\views\style\StylePluginBase::defineOptions(). + * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); @@ -51,7 +51,7 @@ class StyleTest extends StylePluginBase { } /** - * Overrides Drupal\views\Plugin\views\style\StylePluginBase::buildOptionsForm(). + * {@inheritdoc} */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); @@ -94,7 +94,7 @@ class StyleTest extends StylePluginBase { } /** - * Overrides Drupal\views\Plugin\views\style\StylePluginBase::render() + * {@inheritdoc} */ public function render() { $output = ''; diff --git a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php index 9a65d6e51..dfdc0bd4e 100644 --- a/core/modules/views/tests/src/Unit/EntityViewsDataTest.php +++ b/core/modules/views/tests/src/Unit/EntityViewsDataTest.php @@ -774,6 +774,26 @@ class EntityViewsDataTest extends UnitTestCase { $this->assertEquals('entity_link_edit', $data['entity_test']['edit_entity_test']['field']['id']); } + /** + * @covers ::getViewsData + */ + public function testGetViewsDataWithoutEntityOperations() { + // Make sure there is no list builder. The API does not document is + // supports resetting entity handlers, so this might break in the future. + $this->baseEntityType->setListBuilderClass(NULL); + $data = $this->viewsData->getViewsData(); + $this->assertArrayNotHasKey('operations', $data[$this->baseEntityType->getBaseTable()]); + } + + /** + * @covers ::getViewsData + */ + public function testGetViewsDataWithEntityOperations() { + $this->baseEntityType->setListBuilderClass('\Drupal\Core\Entity\EntityListBuilder'); + $data = $this->viewsData->getViewsData(); + $this->assertSame('entity_operations', $data[$this->baseEntityType->getBaseTable()]['operations']['field']['id']); + } + /** * Tests views data for a string field. * 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 b0ebe1314..bae789a93 100644 --- a/core/modules/views/tests/src/Unit/Plugin/display/PathPluginBaseTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/display/PathPluginBaseTest.php @@ -108,6 +108,7 @@ class PathPluginBaseTest extends UnitTestCase { $this->assertEquals('test_id', $route->getDefault('view_id')); $this->assertEquals('page_1', $route->getDefault('display_id')); $this->assertSame(FALSE, $route->getOption('returns_response')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -134,6 +135,7 @@ class PathPluginBaseTest extends UnitTestCase { $this->pathPlugin->collectRoutes($collection); $route = $collection->get('view.test_id.page_1'); $this->assertSame(TRUE, $route->getOption('returns_response')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -161,6 +163,7 @@ class PathPluginBaseTest extends UnitTestCase { $this->assertEquals('test_id', $route->getDefault('view_id')); $this->assertEquals('page_1', $route->getDefault('display_id')); $this->assertEquals(array('arg_0' => 'arg_0'), $route->getOption('_view_argument_map')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -191,6 +194,7 @@ class PathPluginBaseTest extends UnitTestCase { $this->assertEquals('test_id', $route->getDefault('view_id')); $this->assertEquals('page_1', $route->getDefault('display_id')); $this->assertEquals(array('arg_0' => 'arg_0'), $route->getOption('_view_argument_map')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -216,6 +220,7 @@ class PathPluginBaseTest extends UnitTestCase { $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')); } /** @@ -245,6 +250,7 @@ class PathPluginBaseTest extends UnitTestCase { $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')); // Ensure that the test_route_2 is not overridden. $route = $collection->get('test_route_2'); @@ -285,6 +291,7 @@ class PathPluginBaseTest extends UnitTestCase { $this->assertEquals('/test_route/{node}/example', $route->getPath()); $this->assertEquals('test_id', $route->getDefault('view_id')); $this->assertEquals('page_1', $route->getDefault('display_id')); + $this->assertEquals('my views title', $route->getDefault('_title')); $this->assertEquals(array('arg_0' => 'node'), $route->getOption('_view_argument_map')); } @@ -322,6 +329,7 @@ class PathPluginBaseTest extends UnitTestCase { // Ensure that the path did not changed and placeholders are respected. $this->assertEquals('/test_route/{parameter}', $route->getPath()); $this->assertEquals(array('arg_0' => 'parameter'), $route->getOption('_view_argument_map')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -359,6 +367,7 @@ class PathPluginBaseTest extends UnitTestCase { // Ensure that the path did not changed and placeholders are respected kk. $this->assertEquals('/test_route/{parameter}', $route->getPath()); $this->assertEquals(['arg_0' => 'parameter'], $route->getOption('_view_argument_map')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -393,6 +402,7 @@ class PathPluginBaseTest extends UnitTestCase { // Ensure that the path did not changed and placeholders are respected. $this->assertEquals('/test_route/{parameter}/{arg_1}', $route->getPath()); $this->assertEquals(array('arg_0' => 'parameter'), $route->getOption('_view_argument_map')); + $this->assertEquals('my views title', $route->getDefault('_title')); } /** @@ -427,6 +437,10 @@ class PathPluginBaseTest extends UnitTestCase { $view = $this->getMockBuilder('Drupal\views\ViewExecutable') ->disableOriginalConstructor() ->getMock(); + $view->expects($this->any()) + ->method('getTitle') + ->willReturn('my views title'); + $view->storage = $view_entity; // Skip views options caching. diff --git a/core/modules/views/views.install b/core/modules/views/views.install index fd4968439..345b56984 100644 --- a/core/modules/views/views.install +++ b/core/modules/views/views.install @@ -313,3 +313,21 @@ function _views_update_argument_map($displays) { /** * @} End of "addtogroup updates-8.0.0-beta". */ + +/** + * @addtogroup updates-8.0.0-rc + * @{ + */ + +/** + * Clear caches to fix entity operations field. + */ +function views_update_8003() { + // Empty update to cause a cache flush so that views data is rebuilt. Entity + // types that don't implement a list builder cannot have the entity operations + // field. +} + +/** + * @} End of "addtogroup updates-8.0.0-rc". + */ diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index 96d4c7ba0..6a0a141d3 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -863,11 +863,14 @@ function template_preprocess_views_view_row_rss(&$variables) { $variables['title'] = $item->title; $variables['link'] = $item->link; - /** @var \Drupal\Core\Render\RendererInterface $renderer */ - $renderer = \Drupal::service('renderer'); - // We render the item description. It might contain entities, which attach rss - // elements via hook_entity_view, see comment_entity_view(). - $variables['description'] = is_array($item->description) ? $renderer->render($item->description) : $item->description; + // The description is the only place where we should find HTML. + // @see https://validator.w3.org/feed/docs/rss2.html#hrelementsOfLtitemgt + // If we have a render array, render it here and pass the result to the + // template, letting Twig autoescape it. + if (isset($item->description) && is_array($item->description)) { + $variables['description'] = (string) \Drupal::service('renderer')->render($item->description); + } + $variables['item_elements'] = array(); foreach ($item->elements as $element) { if (isset($element['attributes']) && is_array($element['attributes'])) { diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc index 7b2b8867a..2efc08c21 100644 --- a/core/modules/views/views.views.inc +++ b/core/modules/views/views.views.inc @@ -263,7 +263,14 @@ function views_entity_field_label($entity_type, $field_name) { return array($field_name, $all_labels); } // Sort the field labels by it most used label and return the most used one. - arsort($label_counter); + // If the counts are equal, sort by the label to ensure the result is + // deterministic. + uksort($label_counter, function($a, $b) use ($label_counter) { + if ($label_counter[$a] === $label_counter[$b]) { + return strcmp($a, $b); + } + return $label_counter[$a] > $label_counter[$b] ? -1 : 1; + }); $label_counter = array_keys($label_counter); return array($label_counter[0], $all_labels); } diff --git a/core/modules/views_ui/css/views_ui.admin.theme.css b/core/modules/views_ui/css/views_ui.admin.theme.css index 190345b9e..4ab55c465 100644 --- a/core/modules/views_ui/css/views_ui.admin.theme.css +++ b/core/modules/views_ui/css/views_ui.admin.theme.css @@ -352,7 +352,6 @@ td.group-title { padding-left: 0; /* LTR */ } [dir="rtl"] .views-displays .tabs li.tabs__tab:hover { - padding-left: 15px; padding-right: 0; } .views-displays .tabs.secondary a { diff --git a/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php b/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php index b742e7d5a..9a897f272 100644 --- a/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php +++ b/core/modules/views_ui/src/ProxyClass/ParamConverter/ViewUIConverter.php @@ -2,7 +2,7 @@ /** * @file - * Contains Drupal\views_ui\ProxyClass\ParamConverter\ViewUIConverter. + * Contains \Drupal\views_ui\ProxyClass\ParamConverter\ViewUIConverter. */ /** diff --git a/core/modules/views_ui/src/Tests/CustomBooleanTest.php b/core/modules/views_ui/src/Tests/CustomBooleanTest.php index b0ba60a1c..667cb1c9c 100644 --- a/core/modules/views_ui/src/Tests/CustomBooleanTest.php +++ b/core/modules/views_ui/src/Tests/CustomBooleanTest.php @@ -35,7 +35,7 @@ class CustomBooleanTest extends UITestBase { } /** - * Overrides \Drupal\views\Tests\ViewTestBase::dataSet(). + * {@inheritdoc} */ public function dataSet() { $data = parent::dataSet(); diff --git a/core/modules/views_ui/src/Tests/HandlerTest.php b/core/modules/views_ui/src/Tests/HandlerTest.php index 313e14c16..8bfc6867f 100644 --- a/core/modules/views_ui/src/Tests/HandlerTest.php +++ b/core/modules/views_ui/src/Tests/HandlerTest.php @@ -179,7 +179,8 @@ class HandlerTest extends UITestBase { ])->save(); $this->drupalGet('admin/structure/views/nojs/add-handler/content/default/field'); - $this->assertEscaped('Appears in: page, article. Also known as: Content: The giraffe" label '); + $this->assertEscaped('The giraffe" label '); + $this->assertEscaped('Appears in: page, article. Also known as: Content: The giraffe" label'); } /** diff --git a/core/modules/views_ui/src/Tests/QueryTest.php b/core/modules/views_ui/src/Tests/QueryTest.php index cda040c96..ffd910b90 100644 --- a/core/modules/views_ui/src/Tests/QueryTest.php +++ b/core/modules/views_ui/src/Tests/QueryTest.php @@ -25,7 +25,7 @@ class QueryTest extends UITestBase { public static $testViews = array('test_view'); /** - * Overrides \Drupal\views\Tests\ViewTestBase::viewsData(). + * {@inheritdoc} */ protected function viewsData() { $data = parent::viewsData(); diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 75c54731e..47b8e88fe 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -167,7 +167,7 @@ class ViewUI implements ViewEntityInterface { } /** - * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::get(). + * {@inheritdoc} */ public function get($property_name, $langcode = NULL) { if (property_exists($this->storage, $property_name)) { @@ -178,14 +178,14 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Config\Entity\ConfigEntityInterface::setStatus(). + * {@inheritdoc} */ public function setStatus($status) { return $this->storage->setStatus($status); } /** - * Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::set(). + * {@inheritdoc} */ public function set($property_name, $value, $notify = TRUE) { if (property_exists($this->storage, $property_name)) { @@ -941,21 +941,21 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Entity\EntityInterface::id(). + * {@inheritdoc} */ public function id() { return $this->storage->id(); } /** - * Implements \Drupal\Core\Entity\EntityInterface::uuid(). + * {@inheritdoc} */ public function uuid() { return $this->storage->uuid(); } /** - * Implements \Drupal\Core\Entity\EntityInterface::isNew(). + * {@inheritdoc} */ public function isNew() { return $this->storage->isNew(); @@ -969,7 +969,7 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Entity\EntityInterface::bundle(). + * {@inheritdoc} */ public function bundle() { return $this->storage->bundle(); @@ -983,7 +983,7 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Entity\EntityInterface::createDuplicate(). + * {@inheritdoc} */ public function createDuplicate() { return $this->storage->createDuplicate(); @@ -1011,21 +1011,21 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Entity\EntityInterface::delete(). + * {@inheritdoc} */ public function delete() { return $this->storage->delete(); } /** - * Implements \Drupal\Core\Entity\EntityInterface::save(). + * {@inheritdoc} */ public function save() { return $this->storage->save(); } /** - * Implements \Drupal\Core\Entity\EntityInterface::uri(). + * {@inheritdoc} */ public function urlInfo($rel = 'edit-form', array $options = []) { return $this->storage->urlInfo($rel, $options); @@ -1039,14 +1039,14 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Entity\EntityInterface::label(). + * {@inheritdoc} */ public function label() { return $this->storage->label(); } /** - * Implements \Drupal\Core\Entity\EntityInterface::enforceIsNew(). + * {@inheritdoc} */ public function enforceIsNew($value = TRUE) { return $this->storage->enforceIsNew($value); @@ -1074,21 +1074,21 @@ class ViewUI implements ViewEntityInterface { } /** - * Implements \Drupal\Core\Config\Entity\ConfigEntityInterface::enable(). + * {@inheritdoc} */ public function enable() { return $this->storage->enable(); } /** - * Implements \Drupal\Core\Config\Entity\ConfigEntityInterface::disable(). + * {@inheritdoc} */ public function disable() { return $this->storage->disable(); } /** - * Implements \Drupal\Core\Config\Entity\ConfigEntityInterface::status(). + * {@inheritdoc} */ public function status() { return $this->storage->status(); diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 31169cd45..48396388c 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -16,6 +16,7 @@ ./tests/Drupal/Tests ./modules/*/tests/src/Unit ../modules/*/tests/src/Unit + ../profiles/*/tests/src/Unit ../sites/*/modules/*/tests/src/Unit ./vendor @@ -26,6 +27,7 @@ ./tests/Drupal/KernelTests ./modules/*/tests/src/Kernel ../modules/*/tests/src/Kernel + ../profiles/*/tests/src/Kernel ../sites/*/modules/*/tests/src/Kernel ./vendor @@ -36,6 +38,7 @@ ./tests/Drupal/FunctionalTests ./modules/*/tests/src/Functional ../modules/*/tests/src/Functional + ../profiles/*/tests/src/Functional ../sites/*/modules/*/tests/src/Functional ./vendor diff --git a/core/rebuild.php b/core/rebuild.php index 038c15244..b401c5a60 100644 --- a/core/rebuild.php +++ b/core/rebuild.php @@ -40,7 +40,7 @@ catch (HttpExceptionInterface $e) { if (Settings::get('rebuild_access', FALSE) || ($request->get('token') && $request->get('timestamp') && ((REQUEST_TIME - $request->get('timestamp')) < 300) && - ($request->get('token') === Crypt::hmacBase64($request->get('timestamp'), Settings::get('hash_salt'))) + Crypt::hashEquals(Crypt::hmacBase64($request->get('timestamp'), Settings::get('hash_salt')), $request->get('token')) )) { // Clear the APC cache to ensure APC class loader is reset. if (function_exists('apc_clear_cache')) { diff --git a/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php b/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php new file mode 100644 index 000000000..ed11a3157 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Theme/TwigMarkupInterfaceTest.php @@ -0,0 +1,112 @@ +assertEquals($expected, $this->renderObjectWithTwig($variable)); + } + + /** + * Provide test examples. + */ + public function providerTestMarkupInterfaceEmpty() { + return [ + 'empty TranslatableMarkup' => ['', new TranslatableMarkup('')], + 'non-empty TranslatableMarkup' => ['test', new TranslatableMarkup('test')], + 'empty FormattableMarkup' => ['', new FormattableMarkup('', ['@foo' => 'bar'])], + 'non-empty FormattableMarkup' => ['bar', new FormattableMarkup('@foo', ['@foo' => 'bar'])], + 'non-empty Markup' => ['test', Markup::create('test')], + 'empty GeneratedLink' => ['', new GeneratedLink()], + 'non-empty GeneratedLink' => ['test', (new GeneratedLink())->setGeneratedLink('test')], + // Test objects that do not implement \Countable. + 'empty SafeMarkupTestMarkup' => ['', SafeMarkupTestMarkup::create('')], + 'non-empty SafeMarkupTestMarkup' => ['test', SafeMarkupTestMarkup::create('test')], + ]; + } + + /** + * Tests behaviour if a string is translated to become an empty string. + */ + public function testEmptyTranslation() { + $settings = Settings::getAll(); + $settings['locale_custom_strings_en'] = ['' => ['test' => '']]; + // Recreate the settings static. + new Settings($settings); + + $variable = new TranslatableMarkup('test'); + $this->assertEquals('', $this->renderObjectWithTwig($variable)); + + $variable = new TranslatableMarkup('test', [], ['langcode' => 'de']); + $this->assertEquals('test', $this->renderObjectWithTwig($variable)); + } + + /** + * @return \Drupal\Component\Render\MarkupInterface + * The rendered HTML. + */ + protected function renderObjectWithTwig($variable) { + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $context = new RenderContext(); + return $renderer->executeInRenderContext($context, function () use ($renderer, $variable) { + $elements = [ + '#type' => 'inline_template', + '#template' => '{%- if variable is not empty -%}{{ variable }}{%- endif -%}', + '#context' => array('variable' => $variable), + ]; + return $renderer->render($elements); + }); + } + +} + +/** + * Implements MarkupInterface without implementing \Countable + */ +class SafeMarkupTestMarkup implements MarkupInterface { + use MarkupTrait; + + /** + * Overrides MarkupTrait::create() to allow creation with empty strings. + */ + public static function create($string) { + $object = new static(); + $object->string = $string; + return $object; + } + +} diff --git a/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php b/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php index 7c36bda05..9075128ff 100644 --- a/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php +++ b/core/tests/Drupal/Tests/Component/Assertion/InspectorTest.php @@ -1,7 +1,7 @@ 123]); + $redirect->setProtocolVersion('2.0'); + $redirect->setCharset('ibm-943_P14A-2000'); + $redirect->headers->setCookie(new Cookie('name', 'value')); + + // Make a cloned redirect. + $secureRedirect = SecuredRedirectStub::createFromRedirectResponse($redirect); + $this->assertEquals('/magic_redirect_url', $secureRedirect->getTargetUrl()); + $this->assertEquals(301, $secureRedirect->getStatusCode()); + // We pull the headers from the original redirect because there are default headers applied. + $headers1 = $redirect->headers->allPreserveCase(); + $headers2 = $secureRedirect->headers->allPreserveCase(); + // We unset cache headers so we don't test arcane Symfony weirdness. + // https://github.com/symfony/symfony/issues/16171 + unset($headers1['Cache-Control'], $headers2['Cache-Control']); + $this->assertEquals($headers1, $headers2); + $this->assertEquals('2.0', $secureRedirect->getProtocolVersion()); + $this->assertEquals('ibm-943_P14A-2000', $secureRedirect->getCharset()); + $this->assertEquals($redirect->headers->getCookies(), $secureRedirect->headers->getCookies()); + } + +} + +class SecuredRedirectStub extends SecuredRedirectResponse { + + /** + * {@inheritdoc} + */ + protected function isSafe($url) { + // Empty implementation for testing. + return TRUE; + } + +} diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php index 0a2e0ea18..7b9cfaaba 100644 --- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php @@ -22,6 +22,20 @@ use Drupal\Tests\UnitTestCase; */ class SafeMarkupTest extends UnitTestCase { + /** + * The error message of the last error in the error handler. + * + * @var string + */ + protected $lastErrorMessage; + + /** + * The error number of the last error in the error handler. + * + * @var int + */ + protected $lastErrorNumber; + /** * {@inheritdoc} */ @@ -159,6 +173,40 @@ class SafeMarkupTest extends UnitTestCase { return $tests; } + /** + * Custom error handler that saves the last error. + * + * We need this custom error handler because we cannot rely on the error to + * exception conversion as __toString is never allowed to leak any kind of + * exception. + * + * @param int $error_number + * The error number. + * @param string $error_message + * The error message. + */ + public function errorHandler($error_number, $error_message) { + $this->lastErrorNumber = $error_number; + $this->lastErrorMessage = $error_message; + } + + /** + * String formatting with SafeMarkup::format() and an unsupported placeholder. + * + * When you call SafeMarkup::format() with an unsupported placeholder, an + * InvalidArgumentException should be thrown. + */ + public function testUnexpectedFormat() { + + // We set a custom error handler because of https://github.com/sebastianbergmann/phpunit/issues/487 + set_error_handler([$this, 'errorHandler']); + // We want this to trigger an error. + $error = SafeMarkup::format('Broken placeholder: ~placeholder', ['~placeholder' => 'broken'])->__toString(); + restore_error_handler(); + + $this->assertEquals(E_USER_ERROR, $this->lastErrorNumber); + $this->assertEquals('Invalid placeholder: ~placeholder', $this->lastErrorMessage); + } } diff --git a/core/tests/Drupal/Tests/ComposerIntegrationTest.php b/core/tests/Drupal/Tests/ComposerIntegrationTest.php index e24195b0d..4557e94ac 100644 --- a/core/tests/Drupal/Tests/ComposerIntegrationTest.php +++ b/core/tests/Drupal/Tests/ComposerIntegrationTest.php @@ -75,20 +75,33 @@ class ComposerIntegrationTest extends UnitTestCase { * core's composer.json. */ public function testAllModulesReplaced() { + // Assemble a path to core modules. + $module_path = $this->root . '/core/modules'; + + // Grab the 'replace' section of the core composer.json file. $json = json_decode(file_get_contents($this->root . '/core/composer.json')); - $composer_replace_packages = $json->replace; + $composer_replace_packages = (array) $json->replace; - $folders = scandir($this->root . '/core/modules'); + // Get a list of all the files in the module path. + $folders = scandir($module_path); + // Make sure we only deal with directories that aren't . or .. $module_names = []; + $discard = ['.', '..']; foreach ($folders as $file_name) { - if ($file_name !== '.' && $file_name !== '..' && is_dir($file_name)) { + if ((!in_array($file_name, $discard)) && is_dir($module_path . '/' . $file_name)) { $module_names[] = $file_name; } } + // Assert that each core module has a corresponding 'replace' in + // composer.json. foreach ($module_names as $module_name) { - $this->assertTrue(array_key_exists('drupal/'.$module_name, $composer_replace_packages), 'Found ' . $module_name . ' in replace list of composer.json'); + $this->assertArrayHasKey( + 'drupal/' . $module_name, + $composer_replace_packages, + 'Unable to find ' . $module_name . ' in replace list of composer.json' + ); } } diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/ProxyServicesPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/ProxyServicesPassTest.php index 320ce70bf..a383de9f6 100644 --- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/ProxyServicesPassTest.php +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/ProxyServicesPassTest.php @@ -2,8 +2,7 @@ /** * @file - * Contains - * \Drupal\Tests\Core\DependencyInjection\Compiler\ProxyServicesPassTest. + * Contains \Drupal\Tests\Core\DependencyInjection\Compiler\ProxyServicesPassTest. */ namespace Drupal\Tests\Core\DependencyInjection\Compiler; diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php new file mode 100644 index 000000000..c63cef87b --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityFieldManagerTest.php @@ -0,0 +1,824 @@ +container = $this->prophesize(ContainerInterface::class); + \Drupal::setContainer($this->container->reveal()); + + $this->typedDataManager = $this->prophesize(TypedDataManagerInterface::class); + $this->typedDataManager->getDefinition('field_item:boolean')->willReturn([ + 'class' => BooleanItem::class, + ]); + $this->container->get('typed_data_manager')->willReturn($this->typedDataManager->reveal()); + + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->alter('entity_base_field_info', Argument::type('array'), Argument::any())->willReturn(NULL); + $this->moduleHandler->alter('entity_bundle_field_info', Argument::type('array'), Argument::any(), Argument::type('string'))->willReturn(NULL); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); + + $language = new Language(['id' => 'en']); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + $this->languageManager->getCurrentLanguage()->willReturn($language); + $this->languageManager->getLanguages()->willReturn(['en' => (object) ['id' => 'en']]); + + $this->keyValueFactory = $this->prophesize(KeyValueFactoryInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeRepository = $this->prophesize(EntityTypeRepositoryInterface::class); + $this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class); + $this->entityDisplayRepository = $this->prophesize(EntityDisplayRepositoryInterface::class); + + $this->entityFieldManager = new TestEntityFieldManager($this->entityTypeManager->reveal(), $this->entityTypeBundleInfo->reveal(), $this->entityDisplayRepository->reveal(), $this->typedDataManager->reveal(), $this->languageManager->reveal(), $this->keyValueFactory->reveal(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::type('string')) + ->will(function ($args) use ($definitions) { + if (isset($definitions[$args[0]])) { + return $definitions[$args[0]]; + } + throw new PluginNotFoundException($args[0]); + }); + $this->entityTypeManager->getDefinition(Argument::type('string'), FALSE) + ->will(function ($args) use ($definitions) { + if (isset($definitions[$args[0]])) { + return $definitions[$args[0]]; + } + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + + } + + /** + * Tests the getBaseFieldDefinitions() method. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + */ + public function testGetBaseFieldDefinitions() { + $field_definition = $this->setUpEntityWithFieldDefinition(); + + $expected = ['id' => $field_definition]; + $this->assertSame($expected, $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type')); + } + + /** + * Tests the getFieldDefinitions() method. + * + * @covers ::getFieldDefinitions + * @covers ::buildBundleFieldDefinitions + */ + public function testGetFieldDefinitions() { + $field_definition = $this->setUpEntityWithFieldDefinition(); + + $expected = ['id' => $field_definition]; + $this->assertSame($expected, $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_entity_bundle')); + } + + /** + * Tests the getFieldStorageDefinitions() method. + * + * @covers ::getFieldStorageDefinitions + * @covers ::buildFieldStorageDefinitions + */ + public function testGetFieldStorageDefinitions() { + $field_definition = $this->setUpEntityWithFieldDefinition(TRUE); + $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); + $field_storage_definition->getName()->willReturn('field_storage'); + + $definitions = ['field_storage' => $field_storage_definition->reveal()]; + + $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); + $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); + $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); + $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); + + $expected = [ + 'id' => $field_definition, + 'field_storage' => $field_storage_definition->reveal(), + ]; + $this->assertSame($expected, $this->entityFieldManager->getFieldStorageDefinitions('test_entity_type')); + } + + /** + * Tests the getBaseFieldDefinitions() method with a translatable entity type. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + * + * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode + */ + public function testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode($default_langcode_key) { + $this->setUpEntityWithFieldDefinition(FALSE, 'id', ['langcode' => 'langcode', 'default_langcode' => $default_langcode_key]); + + $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); + $field_definition->isTranslatable()->willReturn(TRUE); + + $entity_class = EntityManagerTestEntity::class; + $entity_class::$baseFieldDefinitions += ['langcode' => $field_definition]; + + $this->entityType->isTranslatable()->willReturn(TRUE); + + $definitions = $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type'); + + $this->assertTrue(isset($definitions[$default_langcode_key])); + } + + /** + * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode(). + * + * @return array + * Test data. + */ + public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode() { + return [ + ['default_langcode'], + ['custom_default_langcode_key'], + ]; + } + + /** + * Tests the getBaseFieldDefinitions() method with a translatable entity type. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + * + * @expectedException \LogicException + * @expectedExceptionMessage The Test entity type cannot be translatable as it does not define a translatable "langcode" field. + * + * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode + */ + public function testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode($provide_key, $provide_field, $translatable) { + $keys = $provide_key ? ['langcode' => 'langcode'] : []; + $this->setUpEntityWithFieldDefinition(FALSE, 'id', $keys); + + if ($provide_field) { + $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); + $field_definition->isTranslatable()->willReturn($translatable); + if (!$translatable) { + $field_definition->setTranslatable(!$translatable)->shouldBeCalled(); + } + + $entity_class = EntityManagerTestEntity::class; + $entity_class::$baseFieldDefinitions += ['langcode' => $field_definition->reveal()]; + } + + $this->entityType->isTranslatable()->willReturn(TRUE); + $this->entityType->getLabel()->willReturn('Test'); + + $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type'); + } + + /** + * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode(). + * + * @return array + * Test data. + */ + public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode() { + return [ + [FALSE, TRUE, TRUE], + [TRUE, FALSE, TRUE], + [TRUE, TRUE, FALSE], + ]; + } + + /** + * Tests the getBaseFieldDefinitions() method with caching. + * + * @covers ::getBaseFieldDefinitions + */ + public function testGetBaseFieldDefinitionsWithCaching() { + $field_definition = $this->setUpEntityWithFieldDefinition(); + + $expected = ['id' => $field_definition]; + + $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn(FALSE) + ->shouldBeCalled(); + $this->cacheBackend->set('entity_base_field_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) + ->will(function ($args) { + $data = (object) ['data' => $args[1]]; + $this->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn($data) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type')); + $this->entityFieldManager->testClearEntityFieldInfo(); + $this->assertSame($expected, $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type')); + } + + /** + * Tests the getFieldDefinitions() method with caching. + * + * @covers ::getFieldDefinitions + */ + public function testGetFieldDefinitionsWithCaching() { + $field_definition = $this->setUpEntityWithFieldDefinition(FALSE, 'id'); + + $expected = ['id' => $field_definition]; + + $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn((object) ['data' => $expected]) + ->shouldBeCalledTimes(2); + $this->cacheBackend->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') + ->willReturn(FALSE) + ->shouldBeCalledTimes(1); + $this->cacheBackend->set('entity_bundle_field_definitions:test_entity_type:test_bundle:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) + ->will(function ($args) { + $data = (object) ['data' => $args[1]]; + $this->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') + ->willReturn($data) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $this->assertSame($expected, $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_bundle')); + $this->entityFieldManager->testClearEntityFieldInfo(); + $this->assertSame($expected, $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_bundle')); + } + + /** + * Tests the getFieldStorageDefinitions() method with caching. + * + * @covers ::getFieldStorageDefinitions + */ + public function testGetFieldStorageDefinitionsWithCaching() { + $field_definition = $this->setUpEntityWithFieldDefinition(TRUE, 'id'); + $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); + $field_storage_definition->getName()->willReturn('field_storage'); + + $definitions = ['field_storage' => $field_storage_definition->reveal()]; + + $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); + $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); + $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); + + $expected = [ + 'id' => $field_definition, + 'field_storage' => $field_storage_definition->reveal(), + ]; + + $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') + ->willReturn((object) ['data' => ['id' => $expected['id']]]) + ->shouldBeCalledTimes(2); + $this->cacheBackend->get('entity_field_storage_definitions:test_entity_type:en')->willReturn(FALSE); + + $this->cacheBackend->set('entity_field_storage_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) + ->will(function () use ($expected) { + $this->get('entity_field_storage_definitions:test_entity_type:en') + ->willReturn((object) ['data' => $expected]) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + + $this->assertSame($expected, $this->entityFieldManager->getFieldStorageDefinitions('test_entity_type')); + $this->entityFieldManager->testClearEntityFieldInfo(); + $this->assertSame($expected, $this->entityFieldManager->getFieldStorageDefinitions('test_entity_type')); + } + + /** + * Tests the getBaseFieldDefinitions() method with an invalid definition. + * + * @covers ::getBaseFieldDefinitions + * @covers ::buildBaseFieldDefinitions + * + * @expectedException \LogicException + */ + public function testGetBaseFieldDefinitionsInvalidDefinition() { + $this->setUpEntityWithFieldDefinition(FALSE, 'langcode', ['langcode' => 'langcode']); + + $this->entityType->isTranslatable()->willReturn(TRUE); + $this->entityType->getLabel()->willReturn('the_label'); + + $this->entityFieldManager->getBaseFieldDefinitions('test_entity_type'); + } + + /** + * Tests that getFieldDefinitions() method sets the 'provider' definition key. + * + * @covers ::getFieldDefinitions + * @covers ::buildBundleFieldDefinitions + */ + public function testGetFieldDefinitionsProvider() { + $this->setUpEntityWithFieldDefinition(TRUE); + + $module = 'entity_manager_test_module'; + + // @todo Mock FieldDefinitionInterface once it exposes a proper provider + // setter. See https://www.drupal.org/node/2225961. + $field_definition = $this->prophesize(BaseFieldDefinition::class); + + // We expect two calls as the field definition will be returned from both + // base and bundle entity field info hook implementations. + $field_definition->getProvider()->shouldBeCalled(); + $field_definition->setProvider($module)->shouldBeCalledTimes(2); + $field_definition->setName(0)->shouldBeCalledTimes(2); + $field_definition->setTargetEntityTypeId('test_entity_type')->shouldBeCalled(); + $field_definition->setTargetBundle(NULL)->shouldBeCalled(); + $field_definition->setTargetBundle('test_bundle')->shouldBeCalled(); + + $this->moduleHandler->getImplementations(Argument::type('string'))->willReturn([$module]); + $this->moduleHandler->invoke($module, 'entity_base_field_info', [$this->entityType])->willReturn([$field_definition->reveal()]); + $this->moduleHandler->invoke($module, 'entity_bundle_field_info', Argument::type('array'))->willReturn([$field_definition->reveal()]); + + $this->entityFieldManager->getFieldDefinitions('test_entity_type', 'test_bundle'); + } + + /** + * Prepares an entity that defines a field definition. + * + * @param bool $custom_invoke_all + * (optional) Whether the test will set up its own + * ModuleHandlerInterface::invokeAll() implementation. Defaults to FALSE. + * @param string $field_definition_id + * (optional) The ID to use for the field definition. Defaults to 'id'. + * @param array $entity_keys + * (optional) An array of entity keys for the mocked entity type. Defaults + * to an empty array. + * + * @return \Drupal\Core\Field\BaseFieldDefinition|\Prophecy\Prophecy\ProphecyInterface + * A field definition object. + */ + protected function setUpEntityWithFieldDefinition($custom_invoke_all = FALSE, $field_definition_id = 'id', $entity_keys = []) { + $field_type_manager = $this->prophesize(FieldTypePluginManagerInterface::class); + $field_type_manager->getDefaultStorageSettings('boolean')->willReturn([]); + $field_type_manager->getDefaultFieldSettings('boolean')->willReturn([]); + $this->container->get('plugin.manager.field.field_type')->willReturn($field_type_manager->reveal()); + + $string_translation = $this->prophesize(TranslationInterface::class); + $this->container->get('string_translation')->willReturn($string_translation->reveal()); + + $entity_class = EntityManagerTestEntity::class; + + $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); + $entity_class::$baseFieldDefinitions = [ + $field_definition_id => $field_definition->reveal(), + ]; + $entity_class::$bundleFieldDefinitions = []; + + if (!$custom_invoke_all) { + $this->moduleHandler->getImplementations(Argument::cetera())->willReturn([]); + } + + // Mock the base field definition override. + $override_entity_type = $this->prophesize(EntityTypeInterface::class); + + $this->entityType = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $this->entityType, 'base_field_override' => $override_entity_type]); + + $storage = $this->prophesize(ConfigEntityStorageInterface::class); + $storage->loadMultiple(Argument::type('array'))->willReturn([]); + $this->entityTypeManager->getStorage('base_field_override')->willReturn($storage->reveal()); + + $this->entityType->getClass()->willReturn($entity_class); + $this->entityType->getKeys()->willReturn($entity_keys + ['default_langcode' => 'default_langcode']); + $this->entityType->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); + $this->entityType->isTranslatable()->willReturn(FALSE); + $this->entityType->getProvider()->willReturn('the_provider'); + $this->entityType->id()->willReturn('the_entity_id'); + + return $field_definition->reveal(); + } + + /** + * Tests the clearCachedFieldDefinitions() method. + * + * @covers ::clearCachedFieldDefinitions + */ + public function testClearCachedFieldDefinitions() { + $this->setUpEntityTypeDefinitions(); + + $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); + $this->container->get('cache_tags.invalidator')->willReturn($this->cacheTagsInvalidator->reveal())->shouldBeCalled(); + + $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); + + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + + /** + * @covers ::getExtraFields + */ + function testGetExtraFields() { + $this->setUpEntityTypeDefinitions(); + + $entity_type_id = $this->randomMachineName(); + $bundle = $this->randomMachineName(); + $language_code = 'en'; + $hook_bundle_extra_fields = [ + $entity_type_id => [ + $bundle => [ + 'form' => [ + 'foo_extra_field' => [ + 'label' => 'Foo', + ], + ], + ], + ], + ]; + $processed_hook_bundle_extra_fields = $hook_bundle_extra_fields; + $processed_hook_bundle_extra_fields[$entity_type_id][$bundle] += [ + 'display' => [], + ]; + $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $language_code; + + $language = new Language(['id' => $language_code]); + $this->languageManager->getCurrentLanguage() + ->willReturn($language) + ->shouldBeCalledTimes(1); + + $this->cacheBackend->get($cache_id)->shouldBeCalled(); + + $this->moduleHandler->invokeAll('entity_extra_field_info')->willReturn($hook_bundle_extra_fields); + $this->moduleHandler->alter('entity_extra_field_info', $hook_bundle_extra_fields)->shouldBeCalled(); + + $this->cacheBackend->set($cache_id, $processed_hook_bundle_extra_fields[$entity_type_id][$bundle], Cache::PERMANENT, ['entity_field_info'])->shouldBeCalled(); + + $this->assertSame($processed_hook_bundle_extra_fields[$entity_type_id][$bundle], $this->entityFieldManager->getExtraFields($entity_type_id, $bundle)); + } + + /** + * @covers ::getFieldMap + */ + public function testGetFieldMap() { + $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([])->shouldBeCalled(); + + // Set up a content entity type. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_class = EntityManagerTestEntity::class; + + // Define an ID field definition as a base field. + $id_definition = $this->prophesize(FieldDefinitionInterface::class); + $id_definition->getType()->willReturn('integer'); + $base_field_definitions = [ + 'id' => $id_definition->reveal(), + ]; + $entity_class::$baseFieldDefinitions = $base_field_definitions; + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->getAll()->willReturn([ + 'test_entity_type' => [ + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ], + ]); + + // Set up a non-content entity type. + $non_content_entity_type = $this->prophesize(EntityTypeInterface::class); + + // Mock the base field definition override. + $override_entity_type = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'test_entity_type' => $entity_type, + 'non_fieldable' => $non_content_entity_type, + 'base_field_override' => $override_entity_type, + ]); + + $entity_type->getClass()->willReturn($entity_class); + $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); + $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); + $entity_type->isTranslatable()->shouldBeCalled(); + $entity_type->getProvider()->shouldBeCalled(); + + $non_content_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); + + $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); + + // Set up the entity type bundle info to return two bundles for the + // fieldable entity type. + $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([ + 'first_bundle' => 'first_bundle', + 'second_bundle' => 'second_bundle', + ])->shouldBeCalled(); + $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); + + $expected = [ + 'test_entity_type' => [ + 'id' => [ + 'type' => 'integer', + 'bundles' => ['first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'], + ], + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ] + ]; + $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); + } + + /** + * @covers ::getFieldMap + */ + public function testGetFieldMapFromCache() { + $expected = [ + 'test_entity_type' => [ + 'id' => [ + 'type' => 'integer', + 'bundles' => ['first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'], + ], + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ] + ]; + $this->setUpEntityTypeDefinitions(); + $this->cacheBackend->get('entity_field_map')->willReturn((object) ['data' => $expected]); + + // Call the field map twice to make sure the static cache works. + $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); + $this->assertEquals($expected, $this->entityFieldManager->getFieldMap()); + } + + /** + * @covers ::getFieldMapByFieldType + */ + public function testGetFieldMapByFieldType() { + // Set up a content entity type. + $entity_type = $this->prophesize(ContentEntityTypeInterface::class); + $entity_class = EntityManagerTestEntity::class; + + // Set up the entity type bundle info to return two bundles for the + // fieldable entity type. + $this->entityTypeBundleInfo->getBundleInfo('test_entity_type')->willReturn([ + 'first_bundle' => 'first_bundle', + 'second_bundle' => 'second_bundle', + ])->shouldBeCalled(); + $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([])->shouldBeCalled(); + + // Define an ID field definition as a base field. + $id_definition = $this->prophesize(FieldDefinitionInterface::class); + $id_definition->getType()->willReturn('integer')->shouldBeCalled(); + $base_field_definitions = [ + 'id' => $id_definition->reveal(), + ]; + $entity_class::$baseFieldDefinitions = $base_field_definitions; + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal())->shouldBeCalled(); + $key_value_store->getAll()->willReturn([ + 'test_entity_type' => [ + 'by_bundle' => [ + 'type' => 'string', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ], + ])->shouldBeCalled(); + + // Mock the base field definition override. + $override_entity_type = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'test_entity_type' => $entity_type, + 'base_field_override' => $override_entity_type, + ]); + + $entity_type->getClass()->willReturn($entity_class)->shouldBeCalled(); + $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode'])->shouldBeCalled(); + $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE)->shouldBeCalled(); + $entity_type->isTranslatable()->shouldBeCalled(); + $entity_type->getProvider()->shouldBeCalled(); + + $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE)->shouldBeCalled(); + + $integerFields = $this->entityFieldManager->getFieldMapByFieldType('integer'); + $this->assertCount(1, $integerFields['test_entity_type']); + $this->assertArrayNotHasKey('non_fieldable', $integerFields); + $this->assertArrayHasKey('id', $integerFields['test_entity_type']); + $this->assertArrayNotHasKey('by_bundle', $integerFields['test_entity_type']); + + $stringFields = $this->entityFieldManager->getFieldMapByFieldType('string'); + $this->assertCount(1, $stringFields['test_entity_type']); + $this->assertArrayNotHasKey('non_fieldable', $stringFields); + $this->assertArrayHasKey('by_bundle', $stringFields['test_entity_type']); + $this->assertArrayNotHasKey('id', $stringFields['test_entity_type']); + } + +} + +class TestEntityFieldManager extends EntityFieldManager { + + /** + * Allows the static caches to be cleared. + */ + public function testClearEntityFieldInfo() { + $this->baseFieldDefinitions = []; + $this->fieldDefinitions = []; + $this->fieldStorageDefinitions = []; + } +} + +/** + * Provides a content entity with dummy static method implementations. + */ +abstract class EntityManagerTestEntity implements \Iterator, ContentEntityInterface { + + /** + * The base field definitions. + * + * @var \Drupal\Core\Field\FieldDefinitionInterface[] + */ + public static $baseFieldDefinitions = []; + + /** + * The bundle field definitions. + * + * @var array[] + * Keys are entity type IDs, values are arrays of which the keys are bundle + * names and the values are field definitions. + */ + public static $bundleFieldDefinitions = []; + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + return static::$baseFieldDefinitions; + } + + /** + * {@inheritdoc} + */ + public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + return isset(static::$bundleFieldDefinitions[$entity_type->id()][$bundle]) ? static::$bundleFieldDefinitions[$entity_type->id()][$bundle] : []; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php index 9a67a2654..ec66c7ab7 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php @@ -5,42 +5,16 @@ * Contains \Drupal\Tests\Core\Entity\EntityManagerTest. */ -namespace Drupal\Tests\Core\Entity { +namespace Drupal\Tests\Core\Entity; -use Drupal\Component\Plugin\Discovery\DiscoveryInterface; -use Drupal\Component\Plugin\Exception\PluginNotFoundException; -use Drupal\Core\Cache\Cache; -use Drupal\Core\Cache\CacheBackendInterface; -use Drupal\Core\Cache\CacheTagsInvalidatorInterface; -use Drupal\Core\Config\Entity\ConfigEntityStorage; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\ContentEntityTypeInterface; -use Drupal\Core\Entity\DynamicallyFieldableEntityStorageInterface; -use Drupal\Core\Entity\EntityHandlerBase; -use Drupal\Core\Entity\EntityHandlerInterface; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityManager; -use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\FieldTypePluginManagerInterface; -use Drupal\Core\Field\Plugin\Field\FieldType\BooleanItem; -use Drupal\Core\KeyValueStore\KeyValueFactoryInterface; -use Drupal\Core\KeyValueStore\KeyValueStoreInterface; -use Drupal\Core\Language\Language; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\StringTranslation\TranslationInterface; -use Drupal\Core\TypedData\TypedDataManager; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\EntityTypeRepositoryInterface; use Drupal\Tests\UnitTestCase; use Prophecy\Argument; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @coversDefaultClass \Drupal\Core\Entity\EntityManager @@ -51,79 +25,37 @@ class EntityManagerTest extends UnitTestCase { /** * The entity manager. * - * @var \Drupal\Tests\Core\Entity\TestEntityManager + * @var \Drupal\Core\Entity\EntityManager */ protected $entityManager; /** - * The entity type definition. + * The entity type manager. * - * @var \Drupal\Core\Entity\EntityTypeInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $entityType; + protected $entityTypeManager; /** - * The plugin discovery. + * The entity type repository. * - * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityTypeRepositoryInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $discovery; + protected $entityTypeRepository; /** - * The dependency injection container. + * The entity type bundle info. * - * @var \Symfony\Component\DependencyInjection\ContainerInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $container; + protected $entityTypeBundleInfo; /** - * The module handler. + * The entity field manager. * - * @var \Drupal\Core\Extension\ModuleHandlerInterface|\Prophecy\Prophecy\ProphecyInterface + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface */ - protected $moduleHandler; - - /** - * The cache backend to use. - * - * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $cacheBackend; - - /** - * The cache tags invalidator. - * - * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $cacheTagsInvalidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $languageManager; - - /** - * The typed data manager. - * - * @var \Drupal\Core\TypedData\TypedDataManager|\Prophecy\Prophecy\ProphecyInterface - */ - protected $typedDataManager; - - /** - * The keyvalue factory. - * - * @var \Drupal\Core\KeyValueStore\KeyValueFactoryInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $keyValueFactory; - - /** - * The event dispatcher. - * - * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\Prophecy\Prophecy\ProphecyInterface - */ - protected $eventDispatcher; + protected $entityFieldManager; /** * {@inheritdoc} @@ -131,1601 +63,33 @@ class EntityManagerTest extends UnitTestCase { protected function setUp() { parent::setUp(); - $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); - $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]); - $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL); - $this->moduleHandler->alter('entity_base_field_info', Argument::type('array'), Argument::any())->willReturn(NULL); - $this->moduleHandler->alter('entity_bundle_field_info', Argument::type('array'), Argument::any(), Argument::type('string'))->willReturn(NULL); + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeRepository = $this->prophesize(EntityTypeRepositoryInterface::class); + $this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class); + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); - $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); - $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $this->entityTypeManager->reveal()); + $container->set('entity_type.repository', $this->entityTypeRepository->reveal()); + $container->set('entity_type.bundle.info', $this->entityTypeBundleInfo->reveal()); + $container->set('entity_field.manager', $this->entityFieldManager->reveal()); - $language = new Language(['id' => 'en']); - $this->languageManager = $this->prophesize(LanguageManagerInterface::class); - $this->languageManager->getCurrentLanguage()->willReturn($language); - $this->languageManager->getLanguages()->willReturn(['en' => (object) ['id' => 'en']]); - - $this->typedDataManager = $this->prophesize(TypedDataManager::class); - $this->typedDataManager->getDefinition('field_item:boolean')->willReturn([ - 'class' => BooleanItem::class, - ]); - - $this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class); - - $this->keyValueFactory = $this->prophesize(KeyValueFactoryInterface::class); - - $this->container = $this->prophesize(ContainerInterface::class); - $this->container->get('cache_tags.invalidator')->willReturn($this->cacheTagsInvalidator->reveal()); - $this->container->get('typed_data_manager')->willReturn($this->typedDataManager->reveal()); - \Drupal::setContainer($this->container->reveal()); - - $this->discovery = $this->prophesize(DiscoveryInterface::class); - $translation_manager = $this->prophesize(TranslationInterface::class); - - $this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal(), $this->languageManager->reveal(), $translation_manager->reveal(), $this->getClassResolverStub(), $this->typedDataManager->reveal(), $this->keyValueFactory->reveal(), $this->eventDispatcher->reveal()); - $this->entityManager->setContainer($this->container->reveal()); - $this->entityManager->setDiscovery($this->discovery->reveal()); - } - - /** - * Sets up the entity manager to be tested. - * - * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions - * (optional) An array of entity type definitions. - */ - protected function setUpEntityManager($definitions = array()) { - $class = $this->getMockClass(EntityInterface::class); - foreach ($definitions as $key => $entity_type) { - // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called - // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must - // always be mocked. - $entity_type->getLinkTemplates()->willReturn([]); - - // Give the entity type a legitimate class to return. - $entity_type->getClass()->willReturn($class); - - $definitions[$key] = $entity_type->reveal(); - } - - $this->discovery->getDefinition(Argument::cetera()) - ->will(function ($args) use ($definitions) { - $entity_type_id = $args[0]; - $exception_on_invalid = $args[1]; - if (isset($definitions[$entity_type_id])) { - return $definitions[$entity_type_id]; - } - elseif (!$exception_on_invalid) { - return NULL; - } - else throw new PluginNotFoundException($entity_type_id); - }); - $this->discovery->getDefinitions()->willReturn($definitions); + $this->entityManager = new EntityManager(); + $this->entityManager->setContainer($container); } /** * Tests the clearCachedDefinitions() method. * * @covers ::clearCachedDefinitions - * */ public function testClearCachedDefinitions() { - $this->setUpEntityManager(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_types'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); + $this->entityTypeManager->clearCachedDefinitions()->shouldBeCalled(); + $this->entityTypeRepository->clearCachedDefinitions()->shouldBeCalled(); + $this->entityTypeBundleInfo->clearCachedBundles()->shouldBeCalled(); + $this->entityFieldManager->clearCachedFieldDefinitions()->shouldBeCalled(); $this->entityManager->clearCachedDefinitions(); } - /** - * Tests the processDefinition() method. - * - * @covers ::processDefinition - * - * @expectedException \Drupal\Core\Entity\Exception\InvalidLinkTemplateException - * @expectedExceptionMessage Link template 'canonical' for entity type 'apple' must start with a leading slash, the current link template is 'path/to/apple' - */ - public function testProcessDefinition() { - $apple = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('apple' => $apple)); - - $apple->getLinkTemplates()->willReturn(['canonical' => 'path/to/apple']); - - $definition = $apple->reveal(); - $this->entityManager->processDefinition($definition, 'apple'); - } - - /** - * Tests the getDefinition() method. - * - * @covers ::getDefinition - * - * @dataProvider providerTestGetDefinition - */ - public function testGetDefinition($entity_type_id, $expected) { - $entity = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'apple' => $entity, - 'banana' => $entity, - )); - - $entity_type = $this->entityManager->getDefinition($entity_type_id, FALSE); - if ($expected) { - $this->assertInstanceOf(EntityTypeInterface::class, $entity_type); - } - else { - $this->assertNull($entity_type); - } - } - - /** - * Provides test data for testGetDefinition(). - * - * @return array - * Test data. - */ - public function providerTestGetDefinition() { - return array( - array('apple', TRUE), - array('banana', TRUE), - array('pear', FALSE), - ); - } - - /** - * Tests the getDefinition() method with an invalid definition. - * - * @covers ::getDefinition - * - * @expectedException \Drupal\Component\Plugin\Exception\PluginNotFoundException - * @expectedExceptionMessage The "pear" entity type does not exist. - */ - public function testGetDefinitionInvalidException() { - $this->setUpEntityManager(); - - $this->entityManager->getDefinition('pear', TRUE); - } - - /** - * Tests the hasHandler() method. - * - * @covers ::hasHandler - * - * @dataProvider providerTestHasHandler - */ - public function testHasHandler($entity_type_id, $expected) { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->hasHandlerClass('storage')->willReturn(TRUE); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->hasHandlerClass('storage')->willReturn(FALSE); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $entity_type = $this->entityManager->hasHandler($entity_type_id, 'storage'); - $this->assertSame($expected, $entity_type); - } - - /** - * Provides test data for testHasHandler(). - * - * @return array - * Test data. - */ - public function providerTestHasHandler() { - return array( - array('apple', TRUE), - array('banana', FALSE), - array('pear', FALSE), - ); - } - - /** - * Tests the getStorage() method. - * - * @covers ::getStorage - */ - public function testGetStorage() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getStorage('test_entity_type')); - } - - /** - * Tests the getListBuilder() method. - * - * @covers ::getListBuilder - */ - public function testGetListBuilder() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('list_builder')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getListBuilder('test_entity_type')); - } - - /** - * Tests the getViewBuilder() method. - * - * @covers ::getViewBuilder - */ - public function testGetViewBuilder() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('view_builder')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getViewBuilder('test_entity_type')); - } - - /** - * Tests the getAccessControlHandler() method. - * - * @covers ::getAccessControlHandler - */ - public function testGetAccessControlHandler() { - $class = $this->getTestHandlerClass(); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('access')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->assertInstanceOf($class, $this->entityManager->getAccessControlHandler('test_entity_type')); - } - - /** - * Tests the getFormObject() method. - * - * @covers ::getFormObject - */ - public function testGetFormObject() { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getFormClass('default')->willReturn(TestEntityForm::class); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getFormClass('default')->willReturn(TestEntityFormInjected::class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $apple_form = $this->entityManager->getFormObject('apple', 'default'); - $this->assertInstanceOf(TestEntityForm::class, $apple_form); - $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_form); - $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_form); - - $banana_form = $this->entityManager->getFormObject('banana', 'default'); - $this->assertInstanceOf(TestEntityFormInjected::class, $banana_form); - $this->assertAttributeEquals('yellow', 'color', $banana_form); - - } - - /** - * Tests the getFormObject() method with an invalid operation. - * - * @covers ::getFormObject - * - * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - public function testGetFormObjectInvalidOperation() { - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getFormClass('edit')->willReturn(''); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - $this->entityManager->getFormObject('test_entity_type', 'edit'); - } - - /** - * Tests the getHandler() method. - * - * @covers ::getHandler - */ - public function testGetHandler() { - $class = $this->getTestHandlerClass(); - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getHandlerClass('storage')->willReturn($class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - )); - - $apple_controller = $this->entityManager->getHandler('apple', 'storage'); - $this->assertInstanceOf($class, $apple_controller); - $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_controller); - $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_controller); - } - - /** - * Tests the getHandler() method when no controller is defined. - * - * @covers ::getHandler - * - * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - public function testGetHandlerMissingHandler() { - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn(''); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - $this->entityManager->getHandler('test_entity_type', 'storage'); - } - - /** - * Tests the getBaseFieldDefinitions() method. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - */ - public function testGetBaseFieldDefinitions() { - $field_definition = $this->setUpEntityWithFieldDefinition(); - - $expected = array('id' => $field_definition); - $this->assertSame($expected, $this->entityManager->getBaseFieldDefinitions('test_entity_type')); - } - - /** - * Tests the getFieldDefinitions() method. - * - * @covers ::getFieldDefinitions - * @covers ::buildBundleFieldDefinitions - */ - public function testGetFieldDefinitions() { - $field_definition = $this->setUpEntityWithFieldDefinition(); - - $expected = array('id' => $field_definition); - $this->assertSame($expected, $this->entityManager->getFieldDefinitions('test_entity_type', 'test_entity_bundle')); - } - - /** - * Tests the getFieldStorageDefinitions() method. - * - * @covers ::getFieldStorageDefinitions - * @covers ::buildFieldStorageDefinitions - */ - public function testGetFieldStorageDefinitions() { - $field_definition = $this->setUpEntityWithFieldDefinition(TRUE); - $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); - $field_storage_definition->getName()->willReturn('field_storage'); - - $definitions = ['field_storage' => $field_storage_definition->reveal()]; - - $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); - $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); - $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); - $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); - - $expected = array( - 'id' => $field_definition, - 'field_storage' => $field_storage_definition->reveal(), - ); - $this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type')); - } - - /** - * Tests the getBaseFieldDefinitions() method with a translatable entity type. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - * - * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode - */ - public function testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode($default_langcode_key) { - $this->setUpEntityWithFieldDefinition(FALSE, 'id', array('langcode' => 'langcode', 'default_langcode' => $default_langcode_key)); - - $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); - $field_definition->isTranslatable()->willReturn(TRUE); - - $entity_class = EntityManagerTestEntity::class; - $entity_class::$baseFieldDefinitions += array('langcode' => $field_definition); - - $this->entityType->isTranslatable()->willReturn(TRUE); - - $definitions = $this->entityManager->getBaseFieldDefinitions('test_entity_type'); - - $this->assertTrue(isset($definitions[$default_langcode_key])); - } - - /** - * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode(). - * - * @return array - * Test data. - */ - public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeDefaultLangcode() { - return [ - ['default_langcode'], - ['custom_default_langcode_key'], - ]; - } - - /** - * Tests the getBaseFieldDefinitions() method with a translatable entity type. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - * - * @expectedException \LogicException - * @expectedExceptionMessage The Test entity type cannot be translatable as it does not define a translatable "langcode" field. - * - * @dataProvider providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode - */ - public function testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode($provide_key, $provide_field, $translatable) { - $keys = $provide_key ? array('langcode' => 'langcode') : array(); - $this->setUpEntityWithFieldDefinition(FALSE, 'id', $keys); - - if ($provide_field) { - $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); - $field_definition->isTranslatable()->willReturn($translatable); - if (!$translatable) { - $field_definition->setTranslatable(!$translatable)->shouldBeCalled(); - } - - $entity_class = EntityManagerTestEntity::class; - $entity_class::$baseFieldDefinitions += array('langcode' => $field_definition->reveal()); - } - - $this->entityType->isTranslatable()->willReturn(TRUE); - $this->entityType->getLabel()->willReturn('Test'); - - $this->entityManager->getBaseFieldDefinitions('test_entity_type'); - } - - /** - * Provides test data for testGetBaseFieldDefinitionsTranslatableEntityTypeLangcode(). - * - * @return array - * Test data. - */ - public function providerTestGetBaseFieldDefinitionsTranslatableEntityTypeLangcode() { - return [ - [FALSE, TRUE, TRUE], - [TRUE, FALSE, TRUE], - [TRUE, TRUE, FALSE], - ]; - } - - /** - * Tests the getBaseFieldDefinitions() method with caching. - * - * @covers ::getBaseFieldDefinitions - */ - public function testGetBaseFieldDefinitionsWithCaching() { - $field_definition = $this->setUpEntityWithFieldDefinition(); - - $expected = array('id' => $field_definition); - - $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn(FALSE) - ->shouldBeCalled(); - $this->cacheBackend->set('entity_base_field_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) - ->will(function ($args) { - $data = (object) ['data' => $args[1]]; - $this->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn($data) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - - $this->assertSame($expected, $this->entityManager->getBaseFieldDefinitions('test_entity_type')); - $this->entityManager->testClearEntityFieldInfo(); - $this->assertSame($expected, $this->entityManager->getBaseFieldDefinitions('test_entity_type')); - } - - /** - * Tests the getFieldDefinitions() method with caching. - * - * @covers ::getFieldDefinitions - */ - public function testGetFieldDefinitionsWithCaching() { - $field_definition = $this->setUpEntityWithFieldDefinition(FALSE, 'id'); - - $expected = array('id' => $field_definition); - - $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn((object) array('data' => $expected)) - ->shouldBeCalledTimes(2); - $this->cacheBackend->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') - ->willReturn(FALSE) - ->shouldBeCalledTimes(1); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - $this->cacheBackend->set('entity_bundle_field_definitions:test_entity_type:test_bundle:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) - ->will(function ($args) { - $data = (object) ['data' => $args[1]]; - $this->get('entity_bundle_field_definitions:test_entity_type:test_bundle:en') - ->willReturn($data) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - - $this->assertSame($expected, $this->entityManager->getFieldDefinitions('test_entity_type', 'test_bundle')); - $this->entityManager->testClearEntityFieldInfo(); - $this->assertSame($expected, $this->entityManager->getFieldDefinitions('test_entity_type', 'test_bundle')); - } - - /** - * Tests the getFieldStorageDefinitions() method with caching. - * - * @covers ::getFieldStorageDefinitions - */ - public function testGetFieldStorageDefinitionsWithCaching() { - $field_definition = $this->setUpEntityWithFieldDefinition(TRUE, 'id'); - $field_storage_definition = $this->prophesize(FieldStorageDefinitionInterface::class); - $field_storage_definition->getName()->willReturn('field_storage'); - - $definitions = ['field_storage' => $field_storage_definition->reveal()]; - - $this->moduleHandler->getImplementations('entity_field_storage_info')->willReturn(['example_module']); - $this->moduleHandler->invoke('example_module', 'entity_field_storage_info', [$this->entityType])->willReturn($definitions); - $this->moduleHandler->alter('entity_field_storage_info', $definitions, $this->entityType)->willReturn(NULL); - - $expected = array( - 'id' => $field_definition, - 'field_storage' => $field_storage_definition->reveal(), - ); - - $this->cacheBackend->get('entity_base_field_definitions:test_entity_type:en') - ->willReturn((object) ['data' => ['id' => $expected['id']]]) - ->shouldBeCalledTimes(2); - $this->cacheBackend->get('entity_field_storage_definitions:test_entity_type:en')->willReturn(FALSE); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - $this->cacheBackend->set('entity_field_storage_definitions:test_entity_type:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_field_info']) - ->will(function () use ($expected) { - $this->get('entity_field_storage_definitions:test_entity_type:en') - ->willReturn((object) ['data' => $expected]) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - - - $this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type')); - $this->entityManager->testClearEntityFieldInfo(); - $this->assertSame($expected, $this->entityManager->getFieldStorageDefinitions('test_entity_type')); - } - - /** - * Tests the getBaseFieldDefinitions() method with an invalid definition. - * - * @covers ::getBaseFieldDefinitions - * @covers ::buildBaseFieldDefinitions - * - * @expectedException \LogicException - */ - public function testGetBaseFieldDefinitionsInvalidDefinition() { - $this->setUpEntityWithFieldDefinition(FALSE, 'langcode', array('langcode' => 'langcode')); - - $this->entityType->isTranslatable()->willReturn(TRUE); - $this->entityType->getLabel()->willReturn('the_label'); - - $this->entityManager->getBaseFieldDefinitions('test_entity_type'); - } - - /** - * Tests that getFieldDefinitions() method sets the 'provider' definition key. - * - * @covers ::getFieldDefinitions - * @covers ::buildBundleFieldDefinitions - */ - public function testGetFieldDefinitionsProvider() { - $this->setUpEntityWithFieldDefinition(TRUE); - - $module = 'entity_manager_test_module'; - - // @todo Mock FieldDefinitionInterface once it exposes a proper provider - // setter. See https://www.drupal.org/node/2225961. - $field_definition = $this->prophesize(BaseFieldDefinition::class); - - // We expect two calls as the field definition will be returned from both - // base and bundle entity field info hook implementations. - $field_definition->getProvider()->shouldBeCalled(); - $field_definition->setProvider($module)->shouldBeCalledTimes(2); - $field_definition->setName(0)->shouldBeCalledTimes(2); - $field_definition->setTargetEntityTypeId('test_entity_type')->shouldBeCalled(); - $field_definition->setTargetBundle(NULL)->shouldBeCalled(); - $field_definition->setTargetBundle('test_bundle')->shouldBeCalled(); - - $this->moduleHandler->getImplementations(Argument::type('string'))->willReturn([$module]); - $this->moduleHandler->invoke($module, 'entity_base_field_info', [$this->entityType])->willReturn([$field_definition->reveal()]); - $this->moduleHandler->invoke($module, 'entity_bundle_field_info', Argument::type('array'))->willReturn([$field_definition->reveal()]); - - $this->entityManager->getFieldDefinitions('test_entity_type', 'test_bundle'); - } - - /** - * Prepares an entity that defines a field definition. - * - * @param bool $custom_invoke_all - * (optional) Whether the test will set up its own - * ModuleHandlerInterface::invokeAll() implementation. Defaults to FALSE. - * @param string $field_definition_id - * (optional) The ID to use for the field definition. Defaults to 'id'. - * @param array $entity_keys - * (optional) An array of entity keys for the mocked entity type. Defaults - * to an empty array. - * - * @return \Drupal\Core\Field\BaseFieldDefinition|\Prophecy\Prophecy\ProphecyInterface - * A field definition object. - */ - protected function setUpEntityWithFieldDefinition($custom_invoke_all = FALSE, $field_definition_id = 'id', $entity_keys = array()) { - $field_type_manager = $this->prophesize(FieldTypePluginManagerInterface::class); - $field_type_manager->getDefaultStorageSettings('boolean')->willReturn([]); - $field_type_manager->getDefaultFieldSettings('boolean')->willReturn([]); - $this->container->get('plugin.manager.field.field_type')->willReturn($field_type_manager->reveal()); - - $string_translation = $this->prophesize(TranslationInterface::class); - $this->container->get('string_translation')->willReturn($string_translation->reveal()); - - $entity_class = EntityManagerTestEntity::class; - - $field_definition = $this->prophesize()->willImplement(FieldDefinitionInterface::class)->willImplement(FieldStorageDefinitionInterface::class); - $entity_class::$baseFieldDefinitions = array( - $field_definition_id => $field_definition->reveal(), - ); - $entity_class::$bundleFieldDefinitions = array(); - - if (!$custom_invoke_all) { - $this->moduleHandler->getImplementations(Argument::cetera())->willReturn([]); - } - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->entityType = $this->prophesize(EntityTypeInterface::class); - $this->setUpEntityManager(array('test_entity_type' => $this->entityType, 'base_field_override' => $override_entity_type)); - - $override_entity_type->getClass()->willReturn($entity_class); - $override_entity_type->getHandlerClass('storage')->willReturn(TestConfigEntityStorage::class); - - $this->entityType->getClass()->willReturn($entity_class); - $this->entityType->getKeys()->willReturn($entity_keys + ['default_langcode' => 'default_langcode']); - $this->entityType->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); - $this->entityType->isTranslatable()->willReturn(FALSE); - $this->entityType->getProvider()->willReturn('the_provider'); - $this->entityType->id()->willReturn('the_entity_id'); - - return $field_definition->reveal(); - } - - /** - * Tests the clearCachedFieldDefinitions() method. - * - * @covers ::clearCachedFieldDefinitions - */ - public function testClearCachedFieldDefinitions() { - $this->setUpEntityManager(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $this->entityManager->clearCachedFieldDefinitions(); - } - - /** - * Tests the clearCachedBundles() method. - * - * @covers ::clearCachedBundles - */ - public function testClearCachedBundles() { - $this->setUpEntityManager(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); - - $this->entityManager->clearCachedBundles(); - } - - /** - * Tests the getBundleInfo() method. - * - * @covers ::getBundleInfo - * - * @dataProvider providerTestGetBundleInfo - */ - public function testGetBundleInfo($entity_type_id, $expected) { - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getLabel()->willReturn('Apple'); - $apple->getBundleOf()->willReturn(NULL); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getLabel()->willReturn('Banana'); - $banana->getBundleOf()->willReturn(NULL); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $bundle_info = $this->entityManager->getBundleInfo($entity_type_id); - $this->assertSame($expected, $bundle_info); - } - - /** - * Provides test data for testGetBundleInfo(). - * - * @return array - * Test data. - */ - public function providerTestGetBundleInfo() { - return array( - array('apple', array( - 'apple' => array( - 'label' => 'Apple', - ), - )), - array('banana', array( - 'banana' => array( - 'label' => 'Banana', - ), - )), - array('pear', array()), - ); - } - - /** - * Tests the getAllBundleInfo() method. - * - * @covers ::getAllBundleInfo - */ - public function testGetAllBundleInfo() { - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getLabel()->willReturn('Apple'); - $apple->getBundleOf()->willReturn(NULL); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getLabel()->willReturn('Banana'); - $banana->getBundleOf()->willReturn(NULL); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $this->cacheBackend->get('entity_bundle_info:en')->willReturn(FALSE); - $this->cacheBackend->get('entity_type')->willReturn(FALSE); - $this->cacheBackend->set('entity_type', Argument::any(), Cache::PERMANENT, ['entity_types'])->shouldBeCalled(); - $this->cacheBackend->set('entity_bundle_info:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_bundles']) - ->will(function () { - $this->get('entity_bundle_info:en') - ->willReturn((object) ['data' => 'cached data']) - ->shouldBeCalled(); - }) - ->shouldBeCalled(); - - $this->cacheTagsInvalidator->invalidateTags(['entity_types'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); - $this->cacheTagsInvalidator->invalidateTags(['entity_field_info'])->shouldBeCalled(); - - $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); - - $expected = array( - 'apple' => array( - 'apple' => array( - 'label' => 'Apple', - ), - ), - 'banana' => array( - 'banana' => array( - 'label' => 'Banana', - ), - ), - ); - $bundle_info = $this->entityManager->getAllBundleInfo(); - $this->assertSame($expected, $bundle_info); - - $bundle_info = $this->entityManager->getAllBundleInfo(); - $this->assertSame($expected, $bundle_info); - - $this->entityManager->clearCachedDefinitions(); - - $bundle_info = $this->entityManager->getAllBundleInfo(); - $this->assertSame('cached data', $bundle_info); - } - - /** - * Tests the getEntityTypeLabels() method. - * - * @covers ::getEntityTypeLabels - */ - public function testGetEntityTypeLabels() { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getLabel()->willReturn('Apple'); - $apple->getBundleOf()->willReturn(NULL); - - $banana = $this->prophesize(EntityTypeInterface::class); - $banana->getLabel()->willReturn('Banana'); - $banana->getBundleOf()->willReturn(NULL); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $expected = array( - 'apple' => 'Apple', - 'banana' => 'Banana', - ); - $this->assertSame($expected, $this->entityManager->getEntityTypeLabels()); - } - - /** - * Tests the getTranslationFromContext() method. - * - * @covers ::getTranslationFromContext - */ - public function testGetTranslationFromContext() { - $this->setUpEntityManager(); - - $language = new Language(['id' => 'en']); - $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) - ->willReturn($language) - ->shouldBeCalledTimes(1); - $this->languageManager->getFallbackCandidates(Argument::type('array')) - ->will(function ($args) { - $context = $args[0]; - $candidates = array(); - if (!empty($context['langcode'])) { - $candidates[$context['langcode']] = $context['langcode']; - } - return $candidates; - }) - ->shouldBeCalledTimes(1); - - $translated_entity = $this->prophesize(ContentEntityInterface::class); - - $entity = $this->prophesize(ContentEntityInterface::class); - $entity->getUntranslated()->willReturn($entity); - $entity->language()->willReturn($language); - $entity->hasTranslation(LanguageInterface::LANGCODE_DEFAULT)->willReturn(FALSE); - $entity->hasTranslation('custom_langcode')->willReturn(TRUE); - $entity->getTranslation('custom_langcode')->willReturn($translated_entity->reveal()); - $entity->getTranslationLanguages()->willReturn([new Language(['id' => 'en']), new Language(['id' => 'custom_langcode'])]); - $entity->addCacheContexts(['languages:language_content'])->shouldBeCalled(); - - $this->assertSame($entity->reveal(), $this->entityManager->getTranslationFromContext($entity->reveal())); - $this->assertSame($translated_entity->reveal(), $this->entityManager->getTranslationFromContext($entity->reveal(), 'custom_langcode')); - } - - /** - * @covers ::getExtraFields - */ - function testGetExtraFields() { - $this->setUpEntityManager(); - - $entity_type_id = $this->randomMachineName(); - $bundle = $this->randomMachineName(); - $language_code = 'en'; - $hook_bundle_extra_fields = array( - $entity_type_id => array( - $bundle => array( - 'form' => array( - 'foo_extra_field' => array( - 'label' => 'Foo', - ), - ), - ), - ), - ); - $processed_hook_bundle_extra_fields = $hook_bundle_extra_fields; - $processed_hook_bundle_extra_fields[$entity_type_id][$bundle] += array( - 'display' => array(), - ); - $cache_id = 'entity_bundle_extra_fields:' . $entity_type_id . ':' . $bundle . ':' . $language_code; - - $language = new Language(array('id' => $language_code)); - $this->languageManager->getCurrentLanguage() - ->willReturn($language) - ->shouldBeCalledTimes(1); - - $this->cacheBackend->get($cache_id)->shouldBeCalled(); - - $this->moduleHandler->invokeAll('entity_extra_field_info')->willReturn($hook_bundle_extra_fields); - $this->moduleHandler->alter('entity_extra_field_info', $hook_bundle_extra_fields)->shouldBeCalled(); - - $this->cacheBackend->set($cache_id, $processed_hook_bundle_extra_fields[$entity_type_id][$bundle], Cache::PERMANENT, ['entity_field_info'])->shouldBeCalled(); - - $this->assertSame($processed_hook_bundle_extra_fields[$entity_type_id][$bundle], $this->entityManager->getExtraFields($entity_type_id, $bundle)); - } - - /** - * @covers ::getFieldMap - */ - public function testGetFieldMap() { - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - // Set up a content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_class = EntityManagerTestEntity::class; - - // Define an ID field definition as a base field. - $id_definition = $this->prophesize(FieldDefinitionInterface::class); - $id_definition->getType()->willReturn('integer'); - $base_field_definitions = array( - 'id' => $id_definition->reveal(), - ); - $entity_class::$baseFieldDefinitions = $base_field_definitions; - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->getAll()->willReturn([ - 'test_entity_type' => [ - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ]); - - // Set up a non-content entity type. - $non_content_entity_type = $this->prophesize(EntityTypeInterface::class); - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'test_entity_type' => $entity_type, - 'non_fieldable' => $non_content_entity_type, - 'base_field_override' => $override_entity_type, - )); - - $entity_type->getClass()->willReturn($entity_class); - $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); - $entity_type->getBundleOf()->willReturn(NULL); - $entity_type->id()->willReturn('test_entity_type'); - $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); - $entity_type->isTranslatable()->shouldBeCalled(); - $entity_type->getProvider()->shouldBeCalled(); - - $non_content_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); - $non_content_entity_type->getBundleOf()->willReturn(NULL); - $non_content_entity_type->getLabel()->shouldBeCalled(); - - $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); - $override_entity_type->getHandlerClass('storage')->willReturn(TestConfigEntityStorage::class); - $override_entity_type->getBundleOf()->willReturn(NULL); - $override_entity_type->getLabel()->shouldBeCalled(); - - // Set up the module handler to return two bundles for the fieldable entity - // type. - $this->moduleHandler->alter(Argument::type('string'), Argument::type('array')); - $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([ - 'test_entity_type' => [ - 'first_bundle' => [], - 'second_bundle' => [], - ], - ]); - - $expected = array( - 'test_entity_type' => array( - 'id' => array( - 'type' => 'integer', - 'bundles' => array('first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'), - ), - 'by_bundle' => array( - 'type' => 'string', - 'bundles' => array('second_bundle' => 'second_bundle'), - ), - ) - ); - $this->assertEquals($expected, $this->entityManager->getFieldMap()); - } - - /** - * @covers ::getFieldMap - */ - public function testGetFieldMapFromCache() { - $expected = array( - 'test_entity_type' => array( - 'id' => array( - 'type' => 'integer', - 'bundles' => array('first_bundle' => 'first_bundle', 'second_bundle' => 'second_bundle'), - ), - 'by_bundle' => array( - 'type' => 'string', - 'bundles' => array('second_bundle' => 'second_bundle'), - ), - ) - ); - $this->setUpEntityManager(); - $this->cacheBackend->get('entity_field_map')->willReturn((object) array('data' => $expected)); - - // Call the field map twice to make sure the static cache works. - $this->assertEquals($expected, $this->entityManager->getFieldMap()); - $this->assertEquals($expected, $this->entityManager->getFieldMap()); - } - - /** - * @covers ::getFieldMapByFieldType - */ - public function testGetFieldMapByFieldType() { - // Set up a content entity type. - $entity_type = $this->prophesize(ContentEntityTypeInterface::class); - $entity_class = EntityManagerTestEntity::class; - - // Set up the module handler to return two bundles for the fieldable entity - // type. - $this->moduleHandler->getImplementations('entity_base_field_info')->willReturn([]); - $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([ - 'test_entity_type' => [ - 'first_bundle' => [], - 'second_bundle' => [], - ], - ]); - $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); - - // Define an ID field definition as a base field. - $id_definition = $this->prophesize(FieldDefinitionInterface::class); - $id_definition->getType()->willReturn('integer'); - $base_field_definitions = array( - 'id' => $id_definition->reveal(), - ); - $entity_class::$baseFieldDefinitions = $base_field_definitions; - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->getAll()->willReturn([ - 'test_entity_type' => [ - 'by_bundle' => [ - 'type' => 'string', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ], - ]); - - // Mock the base field definition override. - $override_entity_type = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'test_entity_type' => $entity_type, - 'base_field_override' => $override_entity_type, - )); - - $entity_type->getClass()->willReturn($entity_class); - $entity_type->getKeys()->willReturn(['default_langcode' => 'default_langcode']); - $entity_type->id()->willReturn('test_entity_type'); - $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); - $entity_type->getBundleOf()->shouldBeCalled(); - $entity_type->isTranslatable()->shouldBeCalled(); - $entity_type->getProvider()->shouldBeCalled(); - - $override_entity_type->getClass()->willReturn($entity_class); - $override_entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); - $override_entity_type->getHandlerClass('storage')->willReturn(TestConfigEntityStorage::class); - $override_entity_type->getBundleOf()->shouldBeCalled(); - $override_entity_type->getLabel()->shouldBeCalled(); - - $integerFields = $this->entityManager->getFieldMapByFieldType('integer'); - $this->assertCount(1, $integerFields['test_entity_type']); - $this->assertArrayNotHasKey('non_fieldable', $integerFields); - $this->assertArrayHasKey('id', $integerFields['test_entity_type']); - $this->assertArrayNotHasKey('by_bundle', $integerFields['test_entity_type']); - - $stringFields = $this->entityManager->getFieldMapByFieldType('string'); - $this->assertCount(1, $stringFields['test_entity_type']); - $this->assertArrayNotHasKey('non_fieldable', $stringFields); - $this->assertArrayHasKey('by_bundle', $stringFields['test_entity_type']); - $this->assertArrayNotHasKey('id', $stringFields['test_entity_type']); - } - - /** - * @covers ::onFieldDefinitionCreate - */ - public function testOnFieldDefinitionCreateNewField() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - $field_definition->getType()->willReturn('test_type'); - - $class = $this->getMockClass(DynamicallyFieldableEntityStorageInterface::class); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // The entity manager will instantiate a new object with the given class - // name. Define the mock expectations on that. - $storage = $this->entityManager->getStorage('test_entity_type'); - $storage->expects($this->once()) - ->method('onFieldDefinitionCreate') - ->with($field_definition->reveal()); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([]); - $key_value_store->set('test_entity_type', [ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - ])->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionCreate($field_definition->reveal()); - } - - /** - * @covers ::onFieldDefinitionCreate - */ - public function testOnFieldDefinitionCreateExistingField() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - - $class = $this->getMockClass(DynamicallyFieldableEntityStorageInterface::class); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // The entity manager will instantiate a new object with the given class - // name. Define the mock expectations on that. - $storage = $this->entityManager->getStorage('test_entity_type'); - $storage->expects($this->once()) - ->method('onFieldDefinitionCreate') - ->with($field_definition->reveal()); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['existing_bundle' => 'existing_bundle'], - ], - ]); - $key_value_store->set('test_entity_type', [ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['existing_bundle' => 'existing_bundle', 'test_bundle' => 'test_bundle'], - ], - ]) - ->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionCreate($field_definition->reveal()); - } - - /** - * @covers ::onFieldDefinitionUpdate - */ - public function testOnFieldDefinitionUpdate() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - - $class = $this->getMockClass(DynamicallyFieldableEntityStorageInterface::class); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // The entity manager will instantiate a new object with the given class - // name. Define the mock expectations on that. - $storage = $this->entityManager->getStorage('test_entity_type'); - $storage->expects($this->once()) - ->method('onFieldDefinitionUpdate') - ->with($field_definition->reveal()); - - $this->entityManager->onFieldDefinitionUpdate($field_definition->reveal(), $field_definition->reveal()); - } - - /** - * @covers ::onFieldDefinitionDelete - */ - public function testOnFieldDefinitionDeleteMultipleBundles() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - - $class = $this->getMockClass(DynamicallyFieldableEntityStorageInterface::class); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // The entity manager will instantiate a new object with the given class - // name. Define the mock expectations on that. - $storage = $this->entityManager->getStorage('test_entity_type'); - $storage->expects($this->once()) - ->method('onFieldDefinitionDelete') - ->with($field_definition->reveal()); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - 'second_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - ]); - $key_value_store->set('test_entity_type', [ - 'second_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle'], - ], - ]) - ->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionDelete($field_definition->reveal()); - } - - - /** - * @covers ::onFieldDefinitionDelete - */ - public function testOnFieldDefinitionDeleteSingleBundles() { - $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); - $field_definition->getTargetBundle()->willReturn('test_bundle'); - $field_definition->getName()->willReturn('test_field'); - - $class = $this->getMockClass(DynamicallyFieldableEntityStorageInterface::class); - $entity = $this->prophesize(EntityTypeInterface::class); - $entity->getHandlerClass('storage')->willReturn($class); - $this->setUpEntityManager(array('test_entity_type' => $entity)); - - // The entity manager will instantiate a new object with the given class - // name. Define the mock expectations on that. - $storage = $this->entityManager->getStorage('test_entity_type'); - $storage->expects($this->once()) - ->method('onFieldDefinitionDelete') - ->with($field_definition->reveal()); - - // Set up the stored bundle field map. - $key_value_store = $this->prophesize(KeyValueStoreInterface::class); - $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); - $key_value_store->get('test_entity_type')->willReturn([ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['test_bundle' => 'test_bundle', 'second_bundle' => 'second_bundle'], - ], - ]); - $key_value_store->set('test_entity_type', [ - 'test_field' => [ - 'type' => 'test_type', - 'bundles' => ['second_bundle' => 'second_bundle'], - ], - ]) - ->shouldBeCalled(); - - $this->entityManager->onFieldDefinitionDelete($field_definition->reveal()); - } - - /** - * @covers ::getEntityTypeFromClass - */ - public function testGetEntityTypeFromClass() { - $apple = $this->prophesize(EntityTypeInterface::class); - $banana = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - - $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); - $banana->getClass()->willReturn('\Drupal\mango\Entity\Mango'); - $banana->id() - ->willReturn('banana') - ->shouldBeCalledTimes(2); - - $entity_type_id = $this->entityManager->getEntityTypeFromClass('\Drupal\banana\Entity\Banana'); - $this->assertSame('banana', $entity_type_id); - $entity_type_id = $this->entityManager->getEntityTypeFromClass('\Drupal\mango\Entity\Mango'); - $this->assertSame('banana', $entity_type_id); - } - - /** - * @covers ::getEntityTypeFromClass - * - * @expectedException \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException - * @expectedExceptionMessage The \Drupal\pear\Entity\Pear class does not correspond to an entity type. - */ - public function testGetEntityTypeFromClassNoMatch() { - $apple = $this->prophesize(EntityTypeInterface::class); - $banana = $this->prophesize(EntityTypeInterface::class); - - $this->setUpEntityManager(array( - 'apple' => $apple, - 'banana' => $banana, - )); - - $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); - - $this->entityManager->getEntityTypeFromClass('\Drupal\pear\Entity\Pear'); - } - - /** - * @covers ::getEntityTypeFromClass - * - * @expectedException \Drupal\Core\Entity\Exception\AmbiguousEntityClassException - * @expectedExceptionMessage Multiple entity types found for \Drupal\apple\Entity\Apple. - */ - public function testGetEntityTypeFromClassAmbiguous() { - $boskoop = $this->prophesize(EntityTypeInterface::class); - $boskoop->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - $boskoop->id()->willReturn('boskop'); - - $gala = $this->prophesize(EntityTypeInterface::class); - $gala->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); - $gala->id()->willReturn('gala'); - - $this->setUpEntityManager(array( - 'boskoop' => $boskoop, - 'gala' => $gala, - )); - - $this->entityManager->getEntityTypeFromClass('\Drupal\apple\Entity\Apple'); - } - - /** - * @covers ::getRouteProviders - */ - public function testGetRouteProviders() { - $apple = $this->prophesize(EntityTypeInterface::class); - $apple->getRouteProviderClasses()->willReturn(['default' => TestRouteProvider::class]); - - $this->setUpEntityManager(array( - 'apple' => $apple, - )); - - $apple_route_provider = $this->entityManager->getRouteProviders('apple'); - $this->assertInstanceOf(TestRouteProvider::class, $apple_route_provider['default']); - $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_route_provider['default']); - $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_route_provider['default']); - } - - /** - * Gets a mock controller class name. - * - * @return string - * A mock controller class name. - */ - protected function getTestHandlerClass() { - return get_class($this->getMockForAbstractClass(EntityHandlerBase::class)); - } - -} - -/* - * Provides a content entity with dummy static method implementations. - */ -abstract class EntityManagerTestEntity implements \Iterator, ContentEntityInterface { - - /** - * The base field definitions. - * - * @var \Drupal\Core\Field\FieldDefinitionInterface[] - */ - public static $baseFieldDefinitions = array(); - - /** - * The bundle field definitions. - * - * @var array[] - * Keys are entity type IDs, values are arrays of which the keys are bundle - * names and the values are field definitions. - */ - public static $bundleFieldDefinitions = array(); - - /** - * {@inheritdoc} - */ - public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { - return static::$baseFieldDefinitions; - } - - /** - * {@inheritdoc} - */ - public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - return isset(static::$bundleFieldDefinitions[$entity_type->id()][$bundle]) ? static::$bundleFieldDefinitions[$entity_type->id()][$bundle] : array(); - } - -} - -/** - * Provides a testing version of EntityManager with an empty constructor. - */ -class TestEntityManager extends EntityManager { - - /** - * Sets the discovery for the manager. - * - * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery - * The discovery object. - */ - public function setDiscovery(DiscoveryInterface $discovery) { - $this->discovery = $discovery; - } - - /** - * Allows the static caches to be cleared. - */ - public function testClearEntityFieldInfo() { - $this->baseFieldDefinitions = array(); - $this->fieldDefinitions = array(); - $this->fieldStorageDefinitions = array(); - } - -} - -/** - * Provides a test entity handler that uses injection. - */ -class TestEntityHandlerInjected implements EntityHandlerInterface { - - /** - * The color of the entity type. - * - * @var string - */ - protected $color; - - /** - * Constructs a new TestEntityHandlerInjected. - * - * @param string $color - * The color of the entity type. - */ - public function __construct($color) { - $this->color = $color; - } - - /** - * {@inheritdoc} - */ - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static('yellow'); - } - -} - -/** - * Provides a test entity form. - */ -class TestEntityForm extends EntityHandlerBase { - - /** - * The entity manager. - * - * @var \Drupal\Tests\Core\Entity\TestEntityManager - */ - protected $entityManager; - - /** - * {@inheritdoc} - */ - public function getBaseFormId() { - return 'the_base_form_id'; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'the_form_id'; - } - - /** - * {@inheritdoc} - */ - public function setEntity(EntityInterface $entity) { - return $this; - } - - /** - * {@inheritdoc} - */ - public function setOperation($operation) { - return $this; - } - - /** - * {@inheritdoc} - */ - public function setEntityManager(EntityManagerInterface $entity_manager) { - $this->entityManager = $entity_manager; - return $this; - } - -} - -/** - * Provides a test entity form that uses injection. - */ -class TestEntityFormInjected extends TestEntityForm implements ContainerInjectionInterface { - - /** - * The color of the entity type. - * - * @var string - */ - protected $color; - - /** - * Constructs a new TestEntityFormInjected. - * - * @param string $color - * The color of the entity type. - */ - public function __construct($color) { - $this->color = $color; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static('yellow'); - } - -} - -/** - * Provides a test entity route provider. - */ -class TestRouteProvider extends EntityHandlerBase { - -} - - -/** - * Provides a test config entity storage for base field overrides. - */ -class TestConfigEntityStorage extends ConfigEntityStorage { - - public function __construct($entity_type) { - } - - public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - return new static( - $entity_type - ); - } - - public function loadMultiple(array $ids = NULL) { - return array(); - } -} - -} - -namespace { - - /** - * Implements hook_entity_type_build(). - */ - function entity_manager_test_module_entity_type_build() { - } } diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php new file mode 100644 index 000000000..4d718c79f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityRepositoryTest.php @@ -0,0 +1,94 @@ +entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + + $this->entityRepository = new EntityRepository($this->entityTypeManager->reveal(), $this->languageManager->reveal()); + } + + /** + * Tests the getTranslationFromContext() method. + * + * @covers ::getTranslationFromContext + */ + public function testGetTranslationFromContext() { + $language = new Language(['id' => 'en']); + $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT) + ->willReturn($language) + ->shouldBeCalledTimes(1); + $this->languageManager->getFallbackCandidates(Argument::type('array')) + ->will(function ($args) { + $context = $args[0]; + $candidates = array(); + if (!empty($context['langcode'])) { + $candidates[$context['langcode']] = $context['langcode']; + } + return $candidates; + }) + ->shouldBeCalledTimes(1); + + $translated_entity = $this->prophesize(ContentEntityInterface::class); + + $entity = $this->prophesize(ContentEntityInterface::class); + $entity->getUntranslated()->willReturn($entity); + $entity->language()->willReturn($language); + $entity->hasTranslation(LanguageInterface::LANGCODE_DEFAULT)->willReturn(FALSE); + $entity->hasTranslation('custom_langcode')->willReturn(TRUE); + $entity->getTranslation('custom_langcode')->willReturn($translated_entity->reveal()); + $entity->getTranslationLanguages()->willReturn([new Language(['id' => 'en']), new Language(['id' => 'custom_langcode'])]); + $entity->addCacheContexts(['languages:language_content'])->shouldBeCalled(); + + $this->assertSame($entity->reveal(), $this->entityRepository->getTranslationFromContext($entity->reveal())); + $this->assertSame($translated_entity->reveal(), $this->entityRepository->getTranslationFromContext($entity->reveal(), 'custom_langcode')); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php new file mode 100644 index 000000000..82626ec74 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeBundleInfoTest.php @@ -0,0 +1,274 @@ +moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]); + $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + + $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); + + $language = new Language(['id' => 'en']); + $this->languageManager = $this->prophesize(LanguageManagerInterface::class); + $this->languageManager->getCurrentLanguage()->willReturn($language); + $this->languageManager->getLanguages()->willReturn(['en' => (object) ['id' => 'en']]); + + $this->typedDataManager = $this->prophesize(TypedDataManagerInterface::class); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + + $container = $this->prophesize(ContainerInterface::class); + $container->get('cache_tags.invalidator')->willReturn($this->cacheTagsInvalidator->reveal()); + //$container->get('typed_data_manager')->willReturn($this->typedDataManager->reveal()); + \Drupal::setContainer($container->reveal()); + + $this->entityTypeBundleInfo = new EntityTypeBundleInfo($this->entityTypeManager->reveal(), $this->languageManager->reveal(), $this->moduleHandler->reveal(), $this->typedDataManager->reveal(), $this->cacheBackend->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + + } + + /** + * Tests the clearCachedBundles() method. + * + * @covers ::clearCachedBundles + */ + public function testClearCachedBundles() { + $this->setUpEntityTypeDefinitions(); + + $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); + + $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); + + $this->entityTypeBundleInfo->clearCachedBundles(); + } + + /** + * Tests the getBundleInfo() method. + * + * @covers ::getBundleInfo + * + * @dataProvider providerTestGetBundleInfo + */ + public function testGetBundleInfo($entity_type_id, $expected) { + $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); + $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); + + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getLabel()->willReturn('Apple'); + $apple->getBundleOf()->willReturn(NULL); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getLabel()->willReturn('Banana'); + $banana->getBundleOf()->willReturn(NULL); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + $this->assertSame($expected, $bundle_info); + } + + /** + * Provides test data for testGetBundleInfo(). + * + * @return array + * Test data. + */ + public function providerTestGetBundleInfo() { + return [ + ['apple', [ + 'apple' => [ + 'label' => 'Apple', + ], + ]], + ['banana', [ + 'banana' => [ + 'label' => 'Banana', + ], + ]], + ['pear', []], + ]; + } + + /** + * Tests the getAllBundleInfo() method. + * + * @covers ::getAllBundleInfo + */ + public function testGetAllBundleInfo() { + $this->moduleHandler->invokeAll('entity_bundle_info')->willReturn([]); + $this->moduleHandler->alter('entity_bundle_info', Argument::type('array'))->willReturn(NULL); + + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getLabel()->willReturn('Apple'); + $apple->getBundleOf()->willReturn(NULL); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getLabel()->willReturn('Banana'); + $banana->getBundleOf()->willReturn(NULL); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $this->cacheBackend->get('entity_bundle_info:en')->willReturn(FALSE); + $this->cacheBackend->set('entity_bundle_info:en', Argument::any(), Cache::PERMANENT, ['entity_types', 'entity_bundles']) + ->will(function () { + $this->get('entity_bundle_info:en') + ->willReturn((object) ['data' => 'cached data']) + ->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $this->cacheTagsInvalidator->invalidateTags(['entity_bundles'])->shouldBeCalled(); + + $this->typedDataManager->clearCachedDefinitions()->shouldBeCalled(); + + $expected = [ + 'apple' => [ + 'apple' => [ + 'label' => 'Apple', + ], + ], + 'banana' => [ + 'banana' => [ + 'label' => 'Banana', + ], + ], + ]; + $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo(); + $this->assertSame($expected, $bundle_info); + + $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo(); + $this->assertSame($expected, $bundle_info); + + $this->entityTypeBundleInfo->clearCachedBundles(); + + $bundle_info = $this->entityTypeBundleInfo->getAllBundleInfo(); + $this->assertSame('cached data', $bundle_info); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php new file mode 100644 index 000000000..c2325d423 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php @@ -0,0 +1,510 @@ +moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->moduleHandler->getImplementations('entity_type_build')->willReturn([]); + $this->moduleHandler->alter('entity_type', Argument::type('array'))->willReturn(NULL); + + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + $this->translationManager = $this->prophesize(TranslationInterface::class); + + $this->entityTypeManager = new TestEntityTypeManager(new \ArrayObject(), $this->moduleHandler->reveal(), $this->cacheBackend->reveal(), $this->translationManager->reveal(), $this->getClassResolverStub()); + $this->discovery = $this->prophesize(DiscoveryInterface::class); + $this->entityTypeManager->setDiscovery($this->discovery->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->discovery->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->discovery->getDefinitions()->willReturn($definitions); + + } + + /** + * Tests the hasHandler() method. + * + * @covers ::hasHandler + * + * @dataProvider providerTestHasHandler + */ + public function testHasHandler($entity_type_id, $expected) { + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->hasHandlerClass('storage')->willReturn(TRUE); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->hasHandlerClass('storage')->willReturn(FALSE); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $entity_type = $this->entityTypeManager->hasHandler($entity_type_id, 'storage'); + $this->assertSame($expected, $entity_type); + } + + /** + * Provides test data for testHasHandler(). + * + * @return array + * Test data. + */ + public function providerTestHasHandler() { + return [ + ['apple', TRUE], + ['banana', FALSE], + ['pear', FALSE], + ]; + } + + /** + * Tests the getStorage() method. + * + * @covers ::getStorage + */ + public function testGetStorage() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('storage')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getStorage('test_entity_type')); + } + + /** + * Tests the getListBuilder() method. + * + * @covers ::getListBuilder + */ + public function testGetListBuilder() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('list_builder')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getListBuilder('test_entity_type')); + } + + /** + * Tests the getViewBuilder() method. + * + * @covers ::getViewBuilder + */ + public function testGetViewBuilder() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('view_builder')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getViewBuilder('test_entity_type')); + } + + /** + * Tests the getAccessControlHandler() method. + * + * @covers ::getAccessControlHandler + */ + public function testGetAccessControlHandler() { + $class = $this->getTestHandlerClass(); + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('access')->willReturn($class); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->assertInstanceOf($class, $this->entityTypeManager->getAccessControlHandler('test_entity_type')); + } + + /** + * Tests the getFormObject() method. + * + * @covers ::getFormObject + */ + public function testGetFormObject() { + $entity_manager = $this->prophesize(EntityManagerInterface::class); + $container = $this->prophesize(ContainerInterface::class); + $container->get('entity.manager')->willReturn($entity_manager->reveal()); + \Drupal::setContainer($container->reveal()); + + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getFormClass('default')->willReturn(TestEntityForm::class); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getFormClass('default')->willReturn(TestEntityFormInjected::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $apple_form = $this->entityTypeManager->getFormObject('apple', 'default'); + $this->assertInstanceOf(TestEntityForm::class, $apple_form); + $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_form); + $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_form); + + $banana_form = $this->entityTypeManager->getFormObject('banana', 'default'); + $this->assertInstanceOf(TestEntityFormInjected::class, $banana_form); + $this->assertAttributeEquals('yellow', 'color', $banana_form); + + } + + /** + * Tests the getFormObject() method with an invalid operation. + * + * @covers ::getFormObject + * + * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function testGetFormObjectInvalidOperation() { + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getFormClass('edit')->willReturn(''); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + + $this->entityTypeManager->getFormObject('test_entity_type', 'edit'); + } + + /** + * Tests the getHandler() method. + * + * @covers ::getHandler + */ + public function testGetHandler() { + $class = $this->getTestHandlerClass(); + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getHandlerClass('storage')->willReturn($class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + ]); + + $apple_controller = $this->entityTypeManager->getHandler('apple', 'storage'); + $this->assertInstanceOf($class, $apple_controller); + $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_controller); + $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_controller); + } + + /** + * Tests the getHandler() method when no controller is defined. + * + * @covers ::getHandler + * + * @expectedException \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + public function testGetHandlerMissingHandler() { + $entity = $this->prophesize(EntityTypeInterface::class); + $entity->getHandlerClass('storage')->willReturn(''); + $this->setUpEntityTypeDefinitions(['test_entity_type' => $entity]); + $this->entityTypeManager->getHandler('test_entity_type', 'storage'); + } + + /** + * @covers ::getRouteProviders + */ + public function testGetRouteProviders() { + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getRouteProviderClasses()->willReturn(['default' => TestRouteProvider::class]); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + ]); + + $apple_route_provider = $this->entityTypeManager->getRouteProviders('apple'); + $this->assertInstanceOf(TestRouteProvider::class, $apple_route_provider['default']); + $this->assertAttributeInstanceOf(ModuleHandlerInterface::class, 'moduleHandler', $apple_route_provider['default']); + $this->assertAttributeInstanceOf(TranslationInterface::class, 'stringTranslation', $apple_route_provider['default']); + } + + /** + * Tests the processDefinition() method. + * + * @covers ::processDefinition + * + * @expectedException \Drupal\Core\Entity\Exception\InvalidLinkTemplateException + * @expectedExceptionMessage Link template 'canonical' for entity type 'apple' must start with a leading slash, the current link template is 'path/to/apple' + */ + public function testProcessDefinition() { + $apple = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityTypeDefinitions(['apple' => $apple]); + + $apple->getLinkTemplates()->willReturn(['canonical' => 'path/to/apple']); + + $definition = $apple->reveal(); + $this->entityTypeManager->processDefinition($definition, 'apple'); + } + + /** + * Tests the getDefinition() method. + * + * @covers ::getDefinition + * + * @dataProvider providerTestGetDefinition + */ + public function testGetDefinition($entity_type_id, $expected) { + $entity = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $entity, + 'banana' => $entity, + ]); + + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id, FALSE); + if ($expected) { + $this->assertInstanceOf(EntityTypeInterface::class, $entity_type); + } + else { + $this->assertNull($entity_type); + } + } + + /** + * Provides test data for testGetDefinition(). + * + * @return array + * Test data. + */ + public function providerTestGetDefinition() { + return [ + ['apple', TRUE], + ['banana', TRUE], + ['pear', FALSE], + ]; + } + + /** + * Tests the getDefinition() method with an invalid definition. + * + * @covers ::getDefinition + * + * @expectedException \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @expectedExceptionMessage The "pear" entity type does not exist. + */ + public function testGetDefinitionInvalidException() { + $this->setUpEntityTypeDefinitions(); + + $this->entityTypeManager->getDefinition('pear', TRUE); + } + + /** + * Gets a mock controller class name. + * + * @return string + * A mock controller class name. + */ + protected function getTestHandlerClass() { + return get_class($this->getMockForAbstractClass(EntityHandlerBase::class)); + } + +} + +class TestEntityTypeManager extends EntityTypeManager { + + /** + * Sets the discovery for the manager. + * + * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $discovery + * The discovery object. + */ + public function setDiscovery(DiscoveryInterface $discovery) { + $this->discovery = $discovery; + } + +} + +/** + * Provides a test entity form. + */ +class TestEntityForm extends EntityHandlerBase { + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + public function getBaseFormId() { + return 'the_base_form_id'; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'the_form_id'; + } + + /** + * {@inheritdoc} + */ + public function setEntity(EntityInterface $entity) { + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOperation($operation) { + return $this; + } + + /** + * {@inheritdoc} + */ + public function setEntityManager(EntityManagerInterface $entity_manager) { + $this->entityManager = $entity_manager; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + return $this; + } + +} + +/** + * Provides a test entity form that uses injection. + */ +class TestEntityFormInjected extends TestEntityForm implements ContainerInjectionInterface { + + /** + * The color of the entity type. + * + * @var string + */ + protected $color; + + /** + * Constructs a new TestEntityFormInjected. + * + * @param string $color + * The color of the entity type. + */ + public function __construct($color) { + $this->color = $color; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static('yellow'); + } + +} + +/** + * Provides a test entity route provider. + */ +class TestRouteProvider extends EntityHandlerBase { + +} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeRepositoryTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeRepositoryTest.php new file mode 100644 index 000000000..9ec3853f4 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeRepositoryTest.php @@ -0,0 +1,180 @@ +entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + + $this->entityTypeRepository = new EntityTypeRepository($this->entityTypeManager->reveal()); + } + + /** + * Sets up the entity type manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityTypeDefinitions($definitions = []) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + } + + /** + * Tests the getEntityTypeLabels() method. + * + * @covers ::getEntityTypeLabels + */ + public function testGetEntityTypeLabels() { + $apple = $this->prophesize(EntityTypeInterface::class); + $apple->getLabel()->willReturn('Apple'); + $apple->getBundleOf()->willReturn(NULL); + + $banana = $this->prophesize(EntityTypeInterface::class); + $banana->getLabel()->willReturn('Banana'); + $banana->getBundleOf()->willReturn(NULL); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $expected = [ + 'apple' => 'Apple', + 'banana' => 'Banana', + ]; + $this->assertSame($expected, $this->entityTypeRepository->getEntityTypeLabels()); + } + + /** + * @covers ::getEntityTypeFromClass + */ + public function testGetEntityTypeFromClass() { + $apple = $this->prophesize(EntityTypeInterface::class); + $banana = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + + $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); + $banana->getClass()->willReturn('\Drupal\mango\Entity\Mango'); + $banana->id() + ->willReturn('banana') + ->shouldBeCalledTimes(2); + + $entity_type_id = $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\banana\Entity\Banana'); + $this->assertSame('banana', $entity_type_id); + $entity_type_id = $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\mango\Entity\Mango'); + $this->assertSame('banana', $entity_type_id); + } + + /** + * @covers ::getEntityTypeFromClass + * + * @expectedException \Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException + * @expectedExceptionMessage The \Drupal\pear\Entity\Pear class does not correspond to an entity type. + */ + public function testGetEntityTypeFromClassNoMatch() { + $apple = $this->prophesize(EntityTypeInterface::class); + $banana = $this->prophesize(EntityTypeInterface::class); + + $this->setUpEntityTypeDefinitions([ + 'apple' => $apple, + 'banana' => $banana, + ]); + + $apple->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + $banana->getOriginalClass()->willReturn('\Drupal\banana\Entity\Banana'); + + $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\pear\Entity\Pear'); + } + + /** + * @covers ::getEntityTypeFromClass + * + * @expectedException \Drupal\Core\Entity\Exception\AmbiguousEntityClassException + * @expectedExceptionMessage Multiple entity types found for \Drupal\apple\Entity\Apple. + */ + public function testGetEntityTypeFromClassAmbiguous() { + $boskoop = $this->prophesize(EntityTypeInterface::class); + $boskoop->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + $boskoop->id()->willReturn('boskop'); + + $gala = $this->prophesize(EntityTypeInterface::class); + $gala->getOriginalClass()->willReturn('\Drupal\apple\Entity\Apple'); + $gala->id()->willReturn('gala'); + + $this->setUpEntityTypeDefinitions([ + 'boskoop' => $boskoop, + 'gala' => $gala, + ]); + + $this->entityTypeRepository->getEntityTypeFromClass('\Drupal\apple\Entity\Apple'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php index 71948e1cd..262446e95 100644 --- a/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/InfoParserUnitTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\system\Tests\Extension\InfoParserUnitTest. + * Contains \Drupal\Tests\Core\Extension\InfoParserUnitTest. */ namespace Drupal\Tests\Core\Extension; diff --git a/core/tests/Drupal/Tests/Core/Field/FieldDefinitionListenerTest.php b/core/tests/Drupal/Tests/Core/Field/FieldDefinitionListenerTest.php new file mode 100644 index 000000000..8ca9ae033 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Field/FieldDefinitionListenerTest.php @@ -0,0 +1,275 @@ +keyValueFactory = $this->prophesize(KeyValueFactoryInterface::class); + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class); + $this->cacheBackend = $this->prophesize(CacheBackendInterface::class); + + $this->fieldDefinitionListener = new FieldDefinitionListener($this->entityTypeManager->reveal(), $this->entityFieldManager->reveal(), $this->keyValueFactory->reveal(), $this->cacheBackend->reveal()); + } + + /** + * Sets up the entity manager to be tested. + * + * @param \Drupal\Core\Entity\EntityTypeInterface[]|\Prophecy\Prophecy\ProphecyInterface[] $definitions + * (optional) An array of entity type definitions. + */ + protected function setUpEntityManager($definitions = array()) { + $class = $this->getMockClass(EntityInterface::class); + foreach ($definitions as $key => $entity_type) { + // \Drupal\Core\Entity\EntityTypeInterface::getLinkTemplates() is called + // by \Drupal\Core\Entity\EntityManager::processDefinition() so it must + // always be mocked. + $entity_type->getLinkTemplates()->willReturn([]); + + // Give the entity type a legitimate class to return. + $entity_type->getClass()->willReturn($class); + + $definitions[$key] = $entity_type->reveal(); + } + + $this->entityTypeManager->getDefinition(Argument::cetera()) + ->will(function ($args) use ($definitions) { + $entity_type_id = $args[0]; + $exception_on_invalid = $args[1]; + if (isset($definitions[$entity_type_id])) { + return $definitions[$entity_type_id]; + } + elseif (!$exception_on_invalid) { + return NULL; + } + else throw new PluginNotFoundException($entity_type_id); + }); + $this->entityTypeManager->getDefinitions()->willReturn($definitions); + } + + /** + * @covers ::onFieldDefinitionCreate + */ + public function testOnFieldDefinitionCreateNewField() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + $field_definition->getType()->willReturn('test_type'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionCreate($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([]); + $key_value_store->set('test_entity_type', [ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + ])->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionCreate($field_definition->reveal()); + } + + /** + * @covers ::onFieldDefinitionCreate + */ + public function testOnFieldDefinitionCreateExistingField() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionCreate($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['existing_bundle' => 'existing_bundle'], + ], + ]); + $key_value_store->set('test_entity_type', [ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['existing_bundle' => 'existing_bundle', 'test_bundle' => 'test_bundle'], + ], + ]) + ->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionCreate($field_definition->reveal()); + } + + /** + * @covers ::onFieldDefinitionUpdate + */ + public function testOnFieldDefinitionUpdate() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionUpdate($field_definition->reveal(), $field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + $this->fieldDefinitionListener->onFieldDefinitionUpdate($field_definition->reveal(), $field_definition->reveal()); + } + + /** + * @covers ::onFieldDefinitionDelete + */ + public function testOnFieldDefinitionDeleteMultipleBundles() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionDelete($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + 'second_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + ]); + $key_value_store->set('test_entity_type', [ + 'second_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle'], + ], + ]) + ->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionDelete($field_definition->reveal()); + } + + + /** + * @covers ::onFieldDefinitionDelete + */ + public function testOnFieldDefinitionDeleteSingleBundles() { + $field_definition = $this->prophesize(FieldDefinitionInterface::class); + $field_definition->getTargetEntityTypeId()->willReturn('test_entity_type'); + $field_definition->getTargetBundle()->willReturn('test_bundle'); + $field_definition->getName()->willReturn('test_field'); + + $storage = $this->prophesize(DynamicallyFieldableEntityStorageInterface::class); + $storage->onFieldDefinitionDelete($field_definition->reveal())->shouldBeCalledTimes(1); + $this->entityTypeManager->getStorage('test_entity_type')->willReturn($storage->reveal()); + + $entity = $this->prophesize(EntityTypeInterface::class); + $this->setUpEntityManager(['test_entity_type' => $entity]); + + // Set up the stored bundle field map. + $key_value_store = $this->prophesize(KeyValueStoreInterface::class); + $this->keyValueFactory->get('entity.definitions.bundle_field_map')->willReturn($key_value_store->reveal()); + $key_value_store->get('test_entity_type')->willReturn([ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['test_bundle' => 'test_bundle', 'second_bundle' => 'second_bundle'], + ], + ]); + $key_value_store->set('test_entity_type', [ + 'test_field' => [ + 'type' => 'test_type', + 'bundles' => ['second_bundle' => 'second_bundle'], + ], + ]) + ->shouldBeCalled(); + + $this->fieldDefinitionListener->onFieldDefinitionDelete($field_definition->reveal()); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php index 892b57450..5d1c22d7b 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/Context/ContextTest.php @@ -56,7 +56,7 @@ class ContextTest extends UnitTestCase { * @covers ::getContextValue */ public function testDefaultValue() { - $this->setUpDefaultValue(); + $this->setUpDefaultValue('test'); $context = new Context($this->contextDefinition); $context->setTypedDataManager($this->typedDataManager); @@ -67,7 +67,18 @@ class ContextTest extends UnitTestCase { * @covers ::getContextData */ public function testDefaultDataValue() { - $this->setUpDefaultValue(); + $this->setUpDefaultValue('test'); + + $context = new Context($this->contextDefinition); + $context->setTypedDataManager($this->typedDataManager); + $this->assertEquals($this->typedData, $context->getContextData()); + } + + /** + * @covers ::getContextData + */ + public function testNullDataValue() { + $this->setUpDefaultValue(NULL); $context = new Context($this->contextDefinition); $context->setTypedDataManager($this->typedDataManager); @@ -127,8 +138,11 @@ class ContextTest extends UnitTestCase { /** * Set up mocks for the getDefaultValue() method call. + * + * @param mixed $default_value + * The default value to assign to the mock context definition. */ - protected function setUpDefaultValue() { + protected function setUpDefaultValue($default_value = NULL) { $mock_data_definition = $this->getMock('Drupal\Core\TypedData\DataDefinitionInterface'); $this->contextDefinition = $this->getMockBuilder('Drupal\Core\Plugin\Context\ContextDefinitionInterface') @@ -137,7 +151,7 @@ class ContextTest extends UnitTestCase { $this->contextDefinition->expects($this->once()) ->method('getDefaultValue') - ->willReturn('test'); + ->willReturn($default_value); $this->contextDefinition->expects($this->once()) ->method('getDataDefinition') @@ -147,7 +161,7 @@ class ContextTest extends UnitTestCase { $this->typedDataManager->expects($this->once()) ->method('create') - ->with($mock_data_definition, 'test') + ->with($mock_data_definition, $default_value) ->willReturn($this->typedData); } } diff --git a/core/tests/Drupal/Tests/Core/Render/PlaceholderGeneratorTest.php b/core/tests/Drupal/Tests/Core/Render/PlaceholderGeneratorTest.php new file mode 100644 index 000000000..d2abbc06d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Render/PlaceholderGeneratorTest.php @@ -0,0 +1,59 @@ +placeholderGenerator->createPlaceholder($element); + + $original_placeholder_markup = (string)$build['#markup']; + $processed_placeholder_markup = Html::serialize(Html::load($build['#markup'])); + + $this->assertEquals($original_placeholder_markup, $processed_placeholder_markup); + } + + /** + * @return array + */ + public function providerCreatePlaceholderGeneratesValidHtmlMarkup() { + return [ + 'multiple-arguments' => [['#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', ['foo', 'bar']]]], + 'special-character-&' => [['#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', ['foo&bar']]]], + 'special-character-"' => [['#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', ['foo"bar']]]], + 'special-character-<' => [['#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', ['foo' => [['#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', ['foo>bar']]]], + ]; + + } + +} diff --git a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php index 305963daa..1a99bbf3a 100644 --- a/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php +++ b/core/tests/Drupal/Tests/Core/StringTranslation/TranslationManagerTest.php @@ -66,7 +66,7 @@ class TranslationManagerTest extends UnitTestCase { * Tests translation using placeholders. * * @param string $string - * A string containing the English string to translate. + * A string containing the English text to translate. * @param array $args * An associative array of replacements to make after translation. * @param string $expected_string diff --git a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php index 6b9525ea6..cb09022c4 100644 --- a/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php +++ b/core/tests/Drupal/Tests/Core/Template/TwigExtensionTest.php @@ -200,6 +200,20 @@ class TwigExtensionTest extends UnitTestCase { ]; $result = $twig_extension->safeJoin($twig_environment, $items, '
'); $this->assertEquals('<em>will be escaped</em>
will be markup
will be rendered', $result); + + // Ensure safe_join Twig filter supports Traversable variables. + $items = new \ArrayObject([ + 'will be escaped', + $markup, + ['#markup' => 'will be rendered'], + ]); + $result = $twig_extension->safeJoin($twig_environment, $items, ', '); + $this->assertEquals('<em>will be escaped</em>, will be markup, will be rendered', $result); + + // Ensure safe_join Twig filter supports empty variables. + $items = NULL; + $result = $twig_extension->safeJoin($twig_environment, $items, '
'); + $this->assertEmpty($result); } /** diff --git a/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php new file mode 100644 index 000000000..80eed7a13 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Template/TwigSandboxTest.php @@ -0,0 +1,135 @@ +twig = new \Twig_Environment($loader); + $policy = new TwigSandboxPolicy(); + $sandbox = new \Twig_Extension_Sandbox($policy, TRUE); + $this->twig->addExtension($sandbox); + } + + /** + * Tests that dangerous methods cannot be called in entity objects. + * + * @dataProvider getTwigEntityDangerousMethods + * @expectedException \Twig_Sandbox_SecurityError + */ + public function testEntityDangerousMethods($template) { + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $this->twig->render($template, ['entity' => $entity]); + } + + /** + * Data provider for ::testEntityDangerousMethods. + * + * @return array + */ + public function getTwigEntityDangerousMethods() { + return [ + ['{{ entity.delete }}'], + ['{{ entity.save }}'], + ['{{ entity.create }}'], + ]; + } + + /** + * Tests that prefixed methods can be called from within Twig templates. + * + * Currently "get", "has", and "is" are the only allowed prefixes. + */ + public function testEntitySafePrefixes() { + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('hasLinkTemplate') + ->with('test') + ->willReturn(TRUE); + $result = $this->twig->render('{{ entity.hasLinkTemplate("test") }}', ['entity' => $entity]); + $this->assertTrue((bool)$result, 'Sandbox policy allows has* functions to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('isNew') + ->willReturn(TRUE); + $result = $this->twig->render('{{ entity.isNew }}', ['entity' => $entity]); + $this->assertTrue((bool)$result, 'Sandbox policy allows is* functions to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('getEntityType') + ->willReturn('test'); + $result = $this->twig->render('{{ entity.getEntityType }}', ['entity' => $entity]); + $this->assertEquals($result, 'test', 'Sandbox policy allows get* functions to be called.'); + } + + /** + * Tests that valid methods can be called from within Twig templates. + * + * Currently the following methods are whitelisted: id, label, bundle, and + * get. + */ + public function testEntitySafeMethods() { + $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase') + ->disableOriginalConstructor() + ->getMock(); + $entity->expects($this->atLeastOnce()) + ->method('get') + ->with('title') + ->willReturn('test'); + $result = $this->twig->render('{{ entity.get("title") }}', ['entity' => $entity]); + $this->assertEquals($result, 'test', 'Sandbox policy allows get() to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('id') + ->willReturn('1234'); + $result = $this->twig->render('{{ entity.id }}', ['entity' => $entity]); + $this->assertEquals($result, '1234', 'Sandbox policy allows get() to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('label') + ->willReturn('testing'); + $result = $this->twig->render('{{ entity.label }}', ['entity' => $entity]); + $this->assertEquals($result, 'testing', 'Sandbox policy allows get() to be called.'); + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->atLeastOnce()) + ->method('bundle') + ->willReturn('testing'); + $result = $this->twig->render('{{ entity.bundle }}', ['entity' => $entity]); + $this->assertEquals($result, 'testing', 'Sandbox policy allows get() to be called.'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php index 5ef5ecf47..64a7a2f49 100644 --- a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php @@ -54,18 +54,18 @@ EOS; $module_a = <<<'EOS' setupBasicModules(); $key_value = $this->prophesize(KeyValueStoreInterface::class); - $key_value->get('existing_updates', [])->willReturn([]); - $key_value->set('existing_updates', ['module_a_post_update_a'])->willReturn(NULL); + $key_value->get('existing_updates', []) + ->willReturn([]) + ->shouldBeCalledTimes(1); + $key_value->set('existing_updates', ['module_a_post_update_a']) + ->willReturn(NULL) + ->shouldBeCalledTimes(1); $key_value = $key_value->reveal(); $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ @@ -253,8 +257,12 @@ EOS; public function testRegisterInvokedUpdatesWithMultiple() { $this->setupBasicModules(); $key_value = $this->prophesize(KeyValueStoreInterface::class); - $key_value->get('existing_updates', [])->willReturn([]); - $key_value->set('existing_updates', ['module_a_post_update_a', 'module_a_post_update_b'])->willReturn(NULL); + $key_value->get('existing_updates', []) + ->willReturn([]) + ->shouldBeCalledTimes(1); + $key_value->set('existing_updates', ['module_a_post_update_a', 'module_a_post_update_b']) + ->willReturn(NULL) + ->shouldBeCalledTimes(1); $key_value = $key_value->reveal(); $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ @@ -270,8 +278,12 @@ EOS; public function testRegisterInvokedUpdatesWithExistingUpdates() { $this->setupBasicModules(); $key_value = $this->prophesize(KeyValueStoreInterface::class); - $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']); - $key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_post_update_a'])->willReturn(NULL); + $key_value->get('existing_updates', []) + ->willReturn(['module_a_post_update_b']) + ->shouldBeCalledTimes(1); + $key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_post_update_a']) + ->willReturn(NULL) + ->shouldBeCalledTimes(1); $key_value = $key_value->reveal(); $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ @@ -287,8 +299,12 @@ EOS; public function testFilterOutInvokedUpdatesByModule() { $this->setupBasicModules(); $key_value = $this->prophesize(KeyValueStoreInterface::class); - $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b', 'module_a_post_update_a', 'module_b_post_update_a']); - $key_value->set('existing_updates', ['module_b_post_update_a'])->willReturn(NULL); + $key_value->get('existing_updates', []) + ->willReturn(['module_a_post_update_b', 'module_a_post_update_a', 'module_b_post_update_a']) + ->shouldBeCalledTimes(1); + $key_value->set('existing_updates', ['module_b_post_update_a']) + ->willReturn(NULL) + ->shouldBeCalledTimes(1); $key_value = $key_value->reveal(); $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ diff --git a/core/tests/bootstrap.php b/core/tests/bootstrap.php index 31ac4cd05..626f4ba86 100644 --- a/core/tests/bootstrap.php +++ b/core/tests/bootstrap.php @@ -80,6 +80,13 @@ function drupal_phpunit_get_extension_namespaces($dirs) { return $namespaces; } +// We define the COMPOSER_INSTALL constant, so that PHPUnit knows where to +// autoload from. This is needed for tests run in isolation mode, because +// phpunit.xml.dist is located in a non-default directory relative to the +// PHPUnit executable. +if (!defined('PHPUNIT_COMPOSER_INSTALL')) { + define('PHPUNIT_COMPOSER_INSTALL', __DIR__ . '/../../autoload.php'); +} // Start with classes in known locations. $loader = require __DIR__ . '/../../autoload.php'; $loader->add('Drupal\\Tests', __DIR__); diff --git a/core/themes/bartik/css/components/comments.css b/core/themes/bartik/css/components/comments.css index c0c0c922e..aec85409d 100644 --- a/core/themes/bartik/css/components/comments.css +++ b/core/themes/bartik/css/components/comments.css @@ -108,10 +108,10 @@ .comment__content nav { padding-top: 1px; } -.comment .indented { +.indented { margin-left: 40px; /* LTR */ } -[dir="rtl"] .comment .indented { +[dir="rtl"] .indented { margin-right: 40px; margin-left: 0; } diff --git a/core/themes/bartik/css/components/primary-menu.css b/core/themes/bartik/css/components/primary-menu.css index 8b68e85ee..0451b8a6e 100644 --- a/core/themes/bartik/css/components/primary-menu.css +++ b/core/themes/bartik/css/components/primary-menu.css @@ -11,6 +11,8 @@ } [dir="rtl"] .region-primary-menu .menu { text-align: right; + margin-left: 5px; /* This is required to win over specificity of [dir="rtl"] ul.menu */ + margin-right: 5px; /* This is required to win over specificity of [dir="rtl"] ul.menu */ } .region-primary-menu .menu-item { float: none; @@ -115,6 +117,10 @@ body:not(:target) .region-primary-menu .menu-toggle-target-show:target ~ .menu . padding: 0; text-align: center; } + /* This is required to win over specificity of the global [dir="rtl"] .region-primary-menu .menu */ + [dir="rtl"] .region-primary-menu .menu { + text-align: center; + } .region-primary-menu .menu-item, body:not(:target) .region-primary-menu .menu-item { float: left; /* LTR */ @@ -145,6 +151,10 @@ body:not(:target) .region-primary-menu .menu-toggle-target-show:target ~ .menu . margin-bottom: 5px; padding: 0.9em 5px; } + /* This is required to win over specificity of the global [dir="rtl"] .region-primary-menu .menu a */ + [dir="rtl"] .region-primary-menu .menu a { + padding: 0.9em 5px; + } body:not(:target) .region-primary-menu .menu-toggle { display: none; } diff --git a/core/themes/bartik/css/components/secondary-menu.css b/core/themes/bartik/css/components/secondary-menu.css index 0bf73ff09..c3164dbd0 100644 --- a/core/themes/bartik/css/components/secondary-menu.css +++ b/core/themes/bartik/css/components/secondary-menu.css @@ -8,6 +8,8 @@ } [dir="rtl"] .region-secondary-menu .menu { text-align: left; + margin-right: 10px; /* required to win over specificity of [dir="rtl"] ul.menu */ + margin-left: 10px; } .region-secondary-menu .menu-item { margin: 0; diff --git a/core/themes/bartik/templates/block.html.twig b/core/themes/bartik/templates/block.html.twig index f94013dd3..9d8be22ee 100644 --- a/core/themes/bartik/templates/block.html.twig +++ b/core/themes/bartik/templates/block.html.twig @@ -9,8 +9,7 @@ * - configuration: A list of the block's configuration values. * - label: The configured label for the block. * - label_display: The display settings for the label. - * - module: The module that provided this block plugin. - * - cache: The cache settings. + * - provider: The module or other provider that provided this block plugin. * - Block plugin specific settings will also be stored here. * - content: The content of this block. * - attributes: array of HTML attributes populated by modules, intended to diff --git a/core/themes/bartik/templates/node.html.twig b/core/themes/bartik/templates/node.html.twig index 754b670db..5d509aa55 100644 --- a/core/themes/bartik/templates/node.html.twig +++ b/core/themes/bartik/templates/node.html.twig @@ -4,12 +4,10 @@ * Bartik's theme implementation to display a node. * * Available variables: - * - node: Full node entity. - * - id: The node ID. - * - bundle: The type of the node, for example, "page" or "article". - * - authorid: The user ID of the node author. - * - createdtime: Time the node was published formatted in Unix timestamp. - * - changedtime: Time the node was changed formatted in Unix timestamp. + * - node: The node entity with limited access to object properties and methods. + Only "getter" methods (method names starting with "get", "has", or "is") + and a few common methods such as "id" and "label" are available. Calling + other methods (such as node.delete) will result in an exception. * - label: The title of the node. * - content: All node items. Use {{ content }} to print them all, * or print a subset such as {{ content.field_example }}. Use diff --git a/core/themes/classy/classy.info.yml b/core/themes/classy/classy.info.yml index fb5cb3e29..2b7705b10 100644 --- a/core/themes/classy/classy.info.yml +++ b/core/themes/classy/classy.info.yml @@ -17,3 +17,5 @@ libraries-extend: - classy/dropbutton core/drupal.dialog: - classy/dialog + file/drupal.file: + - classy/file diff --git a/core/themes/classy/css/components/inline-form.css b/core/themes/classy/css/components/inline-form.css index 01a110072..b5201a78c 100644 --- a/core/themes/classy/css/components/inline-form.css +++ b/core/themes/classy/css/components/inline-form.css @@ -12,6 +12,10 @@ margin-right: 0; margin-left: 0.5em; } +/* This is required to win over specificity of [dir="rtl"] .form--inline .form-item */ +[dir="rtl"] .views-filterable-options-controls .form-item { + margin-right: 2%; +} .form--inline .form-item-separator { margin-top: 2.3em; margin-right: 1em; /* LTR */ diff --git a/core/themes/classy/templates/block/block--search-form-block.html.twig b/core/themes/classy/templates/block/block--search-form-block.html.twig index da2809311..667202fb6 100644 --- a/core/themes/classy/templates/block/block--search-form-block.html.twig +++ b/core/themes/classy/templates/block/block--search-form-block.html.twig @@ -9,8 +9,7 @@ * - configuration: A list of the block's configuration values, including: * - label: The configured label for the block. * - label_display: The display settings for the label. - * - module: The module that provided this block plugin. - * - cache: The cache settings. + * - provider: The module or other provider that provided this block plugin. * - Block plugin specific settings will also be stored here. * - content: The content of this block. * - attributes: A list HTML attributes populated by modules, intended to diff --git a/core/themes/classy/templates/block/block--system-menu-block.html.twig b/core/themes/classy/templates/block/block--system-menu-block.html.twig index dcb019a6f..407f8403f 100644 --- a/core/themes/classy/templates/block/block--system-menu-block.html.twig +++ b/core/themes/classy/templates/block/block--system-menu-block.html.twig @@ -9,8 +9,7 @@ * - configuration: A list of the block's configuration values. * - label: The configured label for the block. * - label_display: The display settings for the label. - * - module: The module that provided this block plugin. - * - cache: The cache settings. + * - provider: The module or other provider that provided this block plugin. * - Block plugin specific settings will also be stored here. * - content: The content of this block. * - attributes: HTML attributes for the containing element. diff --git a/core/themes/classy/templates/block/block.html.twig b/core/themes/classy/templates/block/block.html.twig index 8b584b916..fd3311be9 100644 --- a/core/themes/classy/templates/block/block.html.twig +++ b/core/themes/classy/templates/block/block.html.twig @@ -9,8 +9,7 @@ * - configuration: A list of the block's configuration values. * - label: The configured label for the block. * - label_display: The display settings for the label. - * - module: The module that provided this block plugin. - * - cache: The cache settings. + * - provider: The module or other provider that provided this block plugin. * - Block plugin specific settings will also be stored here. * - content: The content of this block. * - attributes: array of HTML attributes populated by modules, intended to diff --git a/core/themes/classy/templates/content/node.html.twig b/core/themes/classy/templates/content/node.html.twig index dbef76d11..5af7c25fa 100644 --- a/core/themes/classy/templates/content/node.html.twig +++ b/core/themes/classy/templates/content/node.html.twig @@ -4,12 +4,10 @@ * Theme override to display a node. * * Available variables: - * - node: Full node entity. - * - id: The node ID. - * - bundle: The type of the node, for example, "page" or "article". - * - authorid: The user ID of the node author. - * - createdtime: Time the node was published formatted in Unix timestamp. - * - changedtime: Time the node was changed formatted in Unix timestamp. + * - node: The node entity with limited access to object properties and methods. + Only "getter" methods (method names starting with "get", "has", or "is") + and a few common methods such as "id" and "label" are available. Calling + other methods (such as node.delete) will result in an exception. * - label: The title of the node. * - content: All node items. Use {{ content }} to print them all, * or print a subset such as {{ content.field_example }}. Use diff --git a/core/themes/classy/templates/form/container.html.twig b/core/themes/classy/templates/form/container.html.twig index 5e5bce8c3..3bd88ec25 100644 --- a/core/themes/classy/templates/form/container.html.twig +++ b/core/themes/classy/templates/form/container.html.twig @@ -3,8 +3,10 @@ * @file * Theme override of a container used to wrap child elements. * - * Used for grouped form items. Can also be used as a #theme_wrapper for any + * Used for grouped form items. Can also be used as a theme wrapper for any * renderable element, to surround it with a
and HTML attributes. + * See the @link forms_api_reference.html Form API reference @endlink for more + * information on the #theme_wrappers render array property. * * Available variables: * - attributes: HTML attributes for the containing element. diff --git a/core/themes/seven/css/base/elements.css b/core/themes/seven/css/base/elements.css index f3167ed33..76fcc4c71 100644 --- a/core/themes/seven/css/base/elements.css +++ b/core/themes/seven/css/base/elements.css @@ -137,6 +137,10 @@ ul { margin-left: 0; margin-right: 1.5em; } +/* This is required to win over specificity of [dir="rtl"] ul */ +[dir="rtl"] .messages__list { + margin-right: 0; +} ol { list-style-type: decimal; margin: 0.25em 0 0.25em 2em; /* LTR */ diff --git a/core/themes/seven/css/components/dialog.css b/core/themes/seven/css/components/dialog.css index d104c82f0..99aaa5393 100644 --- a/core/themes/seven/css/components/dialog.css +++ b/core/themes/seven/css/components/dialog.css @@ -19,7 +19,11 @@ background: #6b6b6b; border-top-left-radius: 5px; border-top-right-radius: 5px; - padding: 15px 49px 15px 15px; + padding: 15px 49px 15px 15px; /* LTR */ +} +[dir="rtl"] .ui-dialog .ui-dialog-titlebar { + padding-left: 49px; + padding-right: 15px; } .ui-dialog .ui-dialog-title { font-size: 1.231em; @@ -103,4 +107,3 @@ .ui-dialog .ajax-progress-throbber .message { display: none; } - diff --git a/core/themes/seven/css/components/dropbutton.component.css b/core/themes/seven/css/components/dropbutton.component.css index 6f085020f..229f1bdd0 100644 --- a/core/themes/seven/css/components/dropbutton.component.css +++ b/core/themes/seven/css/components/dropbutton.component.css @@ -18,10 +18,11 @@ -webkit-font-smoothing: antialiased; text-align: left; /* LTR */ } -[dir="rtl"] .js .dropbutton .dropbutton-action > input, -[dir="rtl"] .js .dropbutton .dropbutton-action > a, -[dir="rtl"] .js .dropbutton .dropbutton-action > button { +[dir="rtl"].js .dropbutton .dropbutton-action > input, +[dir="rtl"].js .dropbutton .dropbutton-action > a, +[dir="rtl"].js .dropbutton .dropbutton-action > button { text-align: right; + margin-left: 0; /* This is required to win over specificity of [dir="rtl"] .dropbutton-multiple .dropbutton .dropbutton-action > * */ } .js .dropbutton-action.last { border-radius: 0 0 0 1em; /* LTR */ diff --git a/core/themes/seven/css/components/menus-and-lists.css b/core/themes/seven/css/components/menus-and-lists.css index d96fe5516..3507e09b0 100644 --- a/core/themes/seven/css/components/menus-and-lists.css +++ b/core/themes/seven/css/components/menus-and-lists.css @@ -34,6 +34,7 @@ ul.inline li { [dir="rtl"] ul.links li, [dir="rtl"] ul.inline li { padding-left: 1em; + padding-right: 0; } ul.inline li { display: inline; diff --git a/core/themes/seven/css/components/tables.css b/core/themes/seven/css/components/tables.css index afc9301bd..3bdf7cc63 100644 --- a/core/themes/seven/css/components/tables.css +++ b/core/themes/seven/css/components/tables.css @@ -108,6 +108,10 @@ th.is-active > a:focus:after { td .item-list ul { margin: 0; } +/* This is required to win over specificity of [dir="rtl"] .item-list ul */ +[dir="rtl"] td .item-list ul { + margin: 0; +} td.is-active { background: none; } diff --git a/core/themes/seven/css/components/tabs.css b/core/themes/seven/css/components/tabs.css index 23c4a48de..28401b425 100644 --- a/core/themes/seven/css/components/tabs.css +++ b/core/themes/seven/css/components/tabs.css @@ -257,6 +257,12 @@ li.tabs__tab a { margin-left: 0; margin-right: -1px; } +/* This is required to win over specificity of [dir="rtl"] .tabs.secondary .tabs__tab */ +[dir="rtl"] .views-displays .tabs.secondary li, +[dir="rtl"] .views-displays .tabs.secondary li.is-active { + padding-left: 0; + padding-right: 0; +} .tabs.secondary .tabs__tab + .tabs__tab { border-top: 1px solid #d9d8d4; } @@ -270,6 +276,11 @@ li.tabs__tab a { border-right: 2px solid #004f80; padding-right: 15px; } +/* This is required to win over specificity of [dir="rtl"] .tabs.secondary .tabs__tab.is-active */ +[dir="rtl"] .views-displays .tabs.secondary li.is-active { + border: 0 none; + padding-right: 0; +} .tabs.secondary .tabs__tab:hover, .tabs.secondary .tabs__tab:focus { color: #008ee6; @@ -282,11 +293,26 @@ li.tabs__tab a { border-right: 2px solid #008ee6; padding-right: 15px; } +/* This is required to win over specificity of [dir="rtl"] .tabs.secondary .tabs__tab:hover */ +[dir="rtl"] .views-displays .tabs li.tabs__tab:hover { + border: 0 none; + padding-right: 0; +} .tabs.secondary a { background-color: transparent; padding: 7px 13px 5px; text-decoration: none; } +/* This is required to win over specificity of [dir="rtl"] li.tabs__tab a */ +[dir="rtl"] .tabs.secondary a { + padding-left: 13px; + padding-right: 13px; +} +/* This is required to win over specificity of [dir="rtl"] .tabs.secondary a */ +[dir="rtl"] .views-displays .tabs.secondary a { + padding-left: 7px; + padding-right: 7px; +} .tabs.secondary .is-active a { color: #004f80; } diff --git a/core/themes/seven/css/components/views-ui.css b/core/themes/seven/css/components/views-ui.css index 8c4afda11..bfd4575c4 100644 --- a/core/themes/seven/css/components/views-ui.css +++ b/core/themes/seven/css/components/views-ui.css @@ -43,6 +43,13 @@ details.fieldset-no-legend { [dir="rtl"] .views-admin a.button, [dir="rtl"] .views-ui-dialog a.button { margin-left: 0; + margin-right: 1em; +} +[dir="rtl"] .views-admin input.form-submit:first-child, +[dir="rtl"] .views-ui-dialog input.form-submit:first-child, +[dir="rtl"] .views-admin a.button:first-child, +[dir="rtl"] .views-ui-dialog a.button:first-child { + margin-right: 0; } .form-radios > .form-item { diff --git a/core/themes/seven/css/theme/maintenance-page.css b/core/themes/seven/css/theme/maintenance-page.css index 678434365..3725ac64d 100644 --- a/core/themes/seven/css/theme/maintenance-page.css +++ b/core/themes/seven/css/theme/maintenance-page.css @@ -165,6 +165,9 @@ padding: 15px; margin: 0.25em 0; } + [dir="rtl"] ul { + margin-right: 0; /* Overrides default [dir="rtl"] ul margin */ + } .layout-sidebar-first { float: left; /* LTR */ width: 35%; diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 7f180ab97..0d573e496 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -223,22 +223,24 @@ $databases = array(); * Location of the site configuration files. * * The $config_directories array specifies the location of file system - * directories used for configuration data. On install, "active" and "sync" - * directories are created for configuration. The sync directory is used for - * configuration imports; the active directory is not used by default, since the - * default storage for active configuration is the database rather than the file - * system (this can be changed; see "Active configuration settings" below). + * directories used for configuration data. On install, the "sync" directory is + * created. This is used for configuration imports. The "active" directory is + * not created by default since the default storage for active configuration is + * the database rather than the file system. (This can be changed. See "Active + * configuration settings" below). * - * The default location for the active and sync directories is inside a - * randomly-named directory in the public files path; this setting allows you to - * override these locations. If you use files for the active configuration, you - * can enhance security by putting the active configuration outside your - * document root. + * The default location for the "sync" directory is inside a randomly-named + * directory in the public files path. The setting below allows you to override + * the "sync" location. + * + * If you use files for the "active" configuration, you can tell the + * Configuration system where this directory is located by adding an entry with + * array key CONFIG_ACTIVE_DIRECTORY. * * Example: * @code * $config_directories = array( - * CONFIG_SYNC_DIRECTORY => '/another/directory/outside/webroot', + * CONFIG_SYNC_DIRECTORY => '/directory/outside/webroot', * ); * @endcode */ @@ -580,6 +582,10 @@ if ($settings['hash_salt']) { * By default, the active configuration is stored in the database in the * {config} table. To use a different storage mechanism for the active * configuration, do the following prior to installing: + * - Create an "active" directory and declare its path in $config_directories + * as explained under the 'Location of the site configuration files' section + * above in this file. To enhance security, you can declare a path that is + * outside your document root. * - Override the 'bootstrap_config_storage' setting here. It must be set to a * callable that returns an object that implements * \Drupal\Core\Config\StorageInterface. diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 8d5b5ef27..030a0de76 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -6,6 +6,8 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'Zumba\\Mink\\Driver\\' => array($vendorDir . '/jcalderonzumba/mink-phantomjs-driver/src'), + 'Zumba\\GastonJS\\' => array($vendorDir . '/jcalderonzumba/gastonjs/src'), 'Zend\\Stdlib\\' => array($vendorDir . '/zendframework/zend-stdlib/src'), 'Zend\\Hydrator\\' => array($vendorDir . '/zendframework/zend-hydrator/src'), 'Zend\\Feed\\' => array($vendorDir . '/zendframework/zend-feed/src'), diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 4c4df5b1c..af7346c27 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -107,7 +107,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/47bb3388cfeae41a38087ac8465a7d08fa92ea2e", + "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/6196fdb001faf681f92db2ae10abafb5815affde", "reference": "47bb3388cfeae41a38087ac8465a7d08fa92ea2e", "shasum": "" }, @@ -3757,5 +3757,127 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com" + }, + { + "name": "jcalderonzumba/gastonjs", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/jcalderonzumba/gastonjs.git", + "reference": "5e231b4df98275c404e1371fc5fadd34f6a121ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jcalderonzumba/gastonjs/zipball/5e231b4df98275c404e1371fc5fadd34f6a121ad", + "reference": "5e231b4df98275c404e1371fc5fadd34f6a121ad", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~5.0|~6.0", + "php": ">=5.4" + }, + "require-dev": { + "phpunit/phpunit": "~4.6", + "silex/silex": "~1.2", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.1" + }, + "time": "2015-10-07 11:40:41", + "type": "phantomjs-api", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "source", + "autoload": { + "psr-4": { + "Zumba\\GastonJS\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Juan Francisco Calderón Zumba", + "email": "juanfcz@gmail.com", + "homepage": "http://github.com/jcalderonzumba" + } + ], + "description": "PhantomJS API based server for webpage automation", + "homepage": "https://github.com/jcalderonzumba/gastonjs", + "keywords": [ + "api", + "automation", + "browser", + "headless", + "phantomjs" + ] + }, + { + "name": "jcalderonzumba/mink-phantomjs-driver", + "version": "dev-master", + "version_normalized": "9999999-dev", + "source": { + "type": "git", + "url": "https://github.com/jcalderonzumba/MinkPhantomJSDriver.git", + "reference": "10d7c48c9a4129463052321b52450d98983c4332" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jcalderonzumba/MinkPhantomJSDriver/zipball/10d7c48c9a4129463052321b52450d98983c4332", + "reference": "10d7c48c9a4129463052321b52450d98983c4332", + "shasum": "" + }, + "require": { + "behat/mink": "~1.6", + "jcalderonzumba/gastonjs": "~1.0", + "php": ">=5.4", + "twig/twig": "~1.8" + }, + "require-dev": { + "phpunit/phpunit": "~4.6", + "silex/silex": "~1.2", + "symfony/css-selector": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "symfony/process": "~2.3" + }, + "time": "2015-10-05 18:24:44", + "type": "mink-driver", + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + }, + "installation-source": "source", + "autoload": { + "psr-4": { + "Zumba\\Mink\\Driver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Juan Francisco Calderón Zumba", + "email": "juanfcz@gmail.com", + "homepage": "http://github.com/jcalderonzumba" + } + ], + "description": "PhantomJS driver for Mink framework", + "homepage": "http://mink.behat.org/", + "keywords": [ + "ajax", + "browser", + "headless", + "javascript", + "phantomjs", + "testing" + ] } ] diff --git a/vendor/jcalderonzumba/gastonjs/.travis.yml b/vendor/jcalderonzumba/gastonjs/.travis.yml new file mode 100644 index 000000000..c64d75510 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/.travis.yml @@ -0,0 +1,37 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + fast_finish: true + include: + - php: 5.4 + env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=weak + - php: 5.6 + env: DEPENDENCIES=dev + allow_failures: + - php: 7.0 + - php: hhvm + +cache: + directories: + - $HOME/.composer/cache/files + +before_install: + - composer self-update + - if [ "$DEPENDENCIES" = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi; + +install: + - composer update $COMPOSER_FLAGS + +script: + - bin/run-tests.sh + +after_script: + - ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {} + - ps axo pid,command | grep php | grep -v grep | awk '{print $1}' | xargs -I {} kill {} diff --git a/vendor/jcalderonzumba/gastonjs/LICENSE b/vendor/jcalderonzumba/gastonjs/LICENSE new file mode 100644 index 000000000..7ba018acd --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Juan Francisco Calderón Zumba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/jcalderonzumba/gastonjs/README.md b/vendor/jcalderonzumba/gastonjs/README.md new file mode 100644 index 000000000..4f8e83d57 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/README.md @@ -0,0 +1,8 @@ +GastonJS for Webpage automation +================================ +[![Build Status](https://travis-ci.org/jcalderonzumba/gastonjs.svg?branch=travis_ci)](https://travis-ci.org/jcalderonzumba/gastonjs) +[![Latest Stable Version](https://poser.pugx.org/jcalderonzumba/gastonjs/v/stable)](https://packagist.org/packages/jcalderonzumba/gastonjs) +[![Total Downloads](https://poser.pugx.org/jcalderonzumba/gastonjs/downloads)](https://packagist.org/packages/jcalderonzumba/gastonjs) + + +For full documentation go to [GastonJS doc](http://gastonjs.readthedocs.org/en/latest/) diff --git a/vendor/jcalderonzumba/gastonjs/bin/run-tests.sh b/vendor/jcalderonzumba/gastonjs/bin/run-tests.sh new file mode 100755 index 000000000..426ec94f1 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/bin/run-tests.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +start_browser_api(){ + CURRENT_DIR=$(pwd) + LOCAL_PHANTOMJS="${CURRENT_DIR}/bin/phantomjs" + if [ -f ${LOCAL_PHANTOMJS} ]; then + ${LOCAL_PHANTOMJS} --ssl-protocol=any --ignore-ssl-errors=true src/Client/main.js 8510 1024 768 2>&1 & + else + phantomjs --ssl-protocol=any --ignore-ssl-errors=true src/Client/main.js 8510 1024 768 2>&1 >> /dev/null & + fi + sleep 2 +} + +stop_services(){ + ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {} + ps axo pid,command | grep php | grep -v grep | grep -v phpstorm | awk '{print $1}' | xargs -I {} kill {} + sleep 2 +} + +mkdir -p /tmp/jcalderonzumba/phantomjs +stop_services +start_browser_api +CURRENT_DIR=$(pwd) +${CURRENT_DIR}/bin/phpunit --configuration unit_tests.xml + diff --git a/vendor/jcalderonzumba/gastonjs/composer.json b/vendor/jcalderonzumba/gastonjs/composer.json new file mode 100644 index 000000000..7e5d96e6a --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/composer.json @@ -0,0 +1,49 @@ +{ + "name": "jcalderonzumba/gastonjs", + "description": "PhantomJS API based server for webpage automation", + "keywords": [ + "phantomjs", + "headless", + "api", + "automation", + "browser" + ], + "homepage": "https://github.com/jcalderonzumba/gastonjs", + "type": "phantomjs-api", + "license": "MIT", + "authors": [ + { + "name": "Juan Francisco Calderón Zumba", + "email": "juanfcz@gmail.com", + "homepage": "http://github.com/jcalderonzumba" + } + ], + "require": { + "php": ">=5.4", + "guzzlehttp/guzzle": "~5.0|~6.0" + }, + "require-dev": { + "symfony/process": "~2.1", + "symfony/phpunit-bridge": "~2.7", + "phpunit/phpunit": "~4.6", + "silex/silex": "~1.2" + }, + "config": { + "bin-dir": "bin" + }, + "autoload": { + "psr-4": { + "Zumba\\GastonJS\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Zumba\\GastonJS\\Tests\\": "tests/unit" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/vendor/jcalderonzumba/gastonjs/mkdocs.yml b/vendor/jcalderonzumba/gastonjs/mkdocs.yml new file mode 100644 index 000000000..51d881f57 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/mkdocs.yml @@ -0,0 +1,40 @@ +site_name: GastonJS Documentation +pages: + - GastonJS introduction: index.md + - GastonJS API : + - 'Introduction': api/index.md + - 'Available commands': api/command-list.md + - 'Navigation commands': + - 'visit': api/commands/navigation/visit.md + - 'current_url': api/commands/navigation/current_url.md + - 'reload': api/commands/navigation/reload.md + - 'go_back': api/commands/navigation/go_back.md + - 'go_forward': api/commands/navigation/go_forward.md + - 'Header commands' : + - 'get_headers': api/commands/headers/get_headers.md + - 'response_headers': api/commands/headers/response_headers.md + - 'set_headers': api/commands/headers/set_headers.md + - 'add_headers': api/commands/headers/add_headers.md + - 'add_header': api/commands/headers/add_header.md + - 'Javascript commands' : + - 'add_extension': api/commands/javascript/add_extension.md + - 'execute': api/commands/javascript/execute.md + - 'evaluate': api/commands/javascript/evaluate.md + - 'set_js_errors': api/commands/javascript/set_js_errors.md + - 'Cookies commands' : + - 'cookies': api/commands/cookies/cookies.md + - 'clear_cookies': api/commands/cookies/clear_cookies.md + - 'cookies_enabled': api/commands/cookies/cookies_enabled.md + - 'remove_cookie': api/commands/cookies/remove_cookie.md + - 'set_cookie': api/commands/cookies/set_cookie.md + - 'Mouse commands': + - 'click': api/commands/mouse/click.md + - 'right_click': api/commands/mouse/right_click.md + - 'hover': api/commands/mouse/hover.md + - 'double_click': api/commands/mouse/double_click.md + - 'click_coordinates': api/commands/mouse/click_coordinates.md + - 'mouse_event': api/commands/mouse/mouse_event.md + - 'Render commands': + - 'render': api/commands/render/render.md + - 'render_base64': api/commands/render/render_base64.md + - GastonJS PHP client: clients/php/index.md diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/Browser.php b/vendor/jcalderonzumba/gastonjs/src/Browser/Browser.php new file mode 100644 index 000000000..5c2a3374e --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/Browser.php @@ -0,0 +1,120 @@ +phantomJSHost = $phantomJSHost; + $this->logger = $logger; + $this->debug = false; + $this->createApiClient(); + } + + /** + * Returns the value of a given element in a page + * @param $pageId + * @param $elementId + * @return mixed + */ + public function value($pageId, $elementId) { + return $this->command('value', $pageId, $elementId); + } + + /** + * Sets a value to a given element in a given page + * @param $pageId + * @param $elementId + * @param $value + * @return mixed + */ + public function set($pageId, $elementId, $value) { + return $this->command('set', $pageId, $elementId, $value); + } + + /** + * Tells whether an element on a page is visible or not + * @param $pageId + * @param $elementId + * @return bool + */ + public function isVisible($pageId, $elementId) { + return $this->command('visible', $pageId, $elementId); + } + + /** + * @param $pageId + * @param $elementId + * @return bool + */ + public function isDisabled($pageId, $elementId) { + return $this->command('disabled', $pageId, $elementId); + } + + /** + * Drag an element to a another in a given page + * @param $pageId + * @param $fromId + * @param $toId + * @return mixed + */ + public function drag($pageId, $fromId, $toId) { + return $this->command('drag', $pageId, $fromId, $toId); + } + + /** + * Selects a value in the given element and page + * @param $pageId + * @param $elementId + * @param $value + * @return mixed + */ + public function select($pageId, $elementId, $value) { + return $this->command('select', $pageId, $elementId, $value); + } + + /** + * Triggers an event to a given element on the given page + * @param $pageId + * @param $elementId + * @param $event + * @return mixed + */ + public function trigger($pageId, $elementId, $event) { + return $this->command('trigger', $pageId, $elementId, $event); + } + + /** + * TODO: not sure what this does, needs to do normalizeKeys + * @param int $pageId + * @param int $elementId + * @param array $keys + * @return mixed + */ + public function sendKeys($pageId, $elementId, $keys) { + return $this->command('send_keys', $pageId, $elementId, $this->normalizeKeys($keys)); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserAuthenticationTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserAuthenticationTrait.php new file mode 100644 index 000000000..7416a76e5 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserAuthenticationTrait.php @@ -0,0 +1,19 @@ +command('set_http_auth', $user, $password); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserBase.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserBase.php new file mode 100644 index 000000000..3ef14d0d7 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserBase.php @@ -0,0 +1,124 @@ +apiClient = new Client(array("base_uri" => $this->getPhantomJSHost())); + } + else { + $this->apiClient = new Client(array("base_url" => $this->getPhantomJSHost())); + } + } + + /** + * TODO: not sure how to do the normalizeKeys stuff fix when needed + * @param $keys + * @return mixed + */ + protected function normalizeKeys($keys) { + return $keys; + } + + /** + * @return Client + */ + public function getApiClient() { + return $this->apiClient; + } + + /** + * @return string + */ + public function getPhantomJSHost() { + return $this->phantomJSHost; + } + + /** + * @return mixed + */ + public function getLogger() { + return $this->logger; + } + + /** + * Restarts the browser + */ + public function restart() { + //TODO: Do we really need to do this?, we are just a client + } + + /** + * Sends a command to the browser + * @throws BrowserError + * @throws \Exception + * @return mixed + */ + public function command() { + try { + $args = func_get_args(); + $commandName = $args[0]; + array_shift($args); + $messageToSend = json_encode(array('name' => $commandName, 'args' => $args)); + /** @var $commandResponse \GuzzleHttp\Psr7\Response|\GuzzleHttp\Message\Response */ + $commandResponse = $this->getApiClient()->post("/api", array("body" => $messageToSend)); + $jsonResponse = json_decode($commandResponse->getBody(), TRUE); + } catch (ServerException $e) { + $jsonResponse = json_decode($e->getResponse()->getBody()->getContents(), true); + } catch (ConnectException $e) { + throw new DeadClient($e->getMessage(), $e->getCode(), $e); + } catch (\Exception $e) { + throw $e; + } + + if (isset($jsonResponse['error'])) { + throw $this->getErrorClass($jsonResponse); + } + + return $jsonResponse['response']; + } + + /** + * @param $error + * @return BrowserError + */ + protected function getErrorClass($error) { + $errorClassMap = array( + 'Poltergeist.JavascriptError' => "Zumba\\GastonJS\\Exception\\JavascriptError", + 'Poltergeist.FrameNotFound' => "Zumba\\GastonJS\\Exception\\FrameNotFound", + 'Poltergeist.InvalidSelector' => "Zumba\\GastonJS\\Exception\\InvalidSelector", + 'Poltergeist.StatusFailError' => "Zumba\\GastonJS\\Exception\\StatusFailError", + 'Poltergeist.NoSuchWindowError' => "Zumba\\GastonJS\\Exception\\NoSuchWindowError", + 'Poltergeist.ObsoleteNode' => "Zumba\\GastonJS\\Exception\\ObsoleteNode" + ); + if (isset($error['error']['name']) && isset($errorClassMap[$error["error"]["name"]])) { + return new $errorClassMap[$error["error"]["name"]]($error); + } + + return new BrowserError($error); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserConfigurationTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserConfigurationTrait.php new file mode 100644 index 000000000..0db7f07ac --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserConfigurationTrait.php @@ -0,0 +1,39 @@ +command('set_js_errors', $enabled); + } + + /** + * Set a blacklist of urls that we are not supposed to load + * @param array $blackList + * @return bool + */ + public function urlBlacklist($blackList) { + return $this->command('set_url_blacklist', $blackList); + } + + /** + * Set the debug mode on the browser + * @param bool $enable + * @return bool + */ + public function debug($enable = false) { + $this->debug = $enable; + return $this->command('set_debug', $this->debug); + } + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserCookieTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserCookieTrait.php new file mode 100644 index 000000000..e2318a4fa --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserCookieTrait.php @@ -0,0 +1,65 @@ +command('cookies'); + $objCookies = array(); + foreach ($cookies as $cookie) { + $objCookies[$cookie["name"]] = new Cookie($cookie); + } + return $objCookies; + } + + /** + * Sets a cookie on the browser, expires times is set in seconds + * @param $cookie + * @return mixed + */ + public function setCookie($cookie) { + //TODO: add error control when the cookie array is not valid + if (isset($cookie["expires"])) { + $cookie["expires"] = intval($cookie["expires"]) * 1000; + } + $cookie['value'] = urlencode($cookie['value']); + return $this->command('set_cookie', $cookie); + } + + /** + * Deletes a cookie on the browser if exists + * @param $cookieName + * @return bool + */ + public function removeCookie($cookieName) { + return $this->command('remove_cookie', $cookieName); + } + + /** + * Clear all the cookies + * @return bool + */ + public function clearCookies() { + return $this->command('clear_cookies'); + } + + /** + * Enables or disables the cookies con phantomjs + * @param bool $enabled + * @return bool + */ + public function cookiesEnabled($enabled = true) { + return $this->command('cookies_enabled', $enabled); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserFileTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserFileTrait.php new file mode 100644 index 000000000..51fc745f6 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserFileTrait.php @@ -0,0 +1,20 @@ +command('select_file', $pageId, $elementId, $value); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserFrameTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserFrameTrait.php new file mode 100644 index 000000000..edefe5eb1 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserFrameTrait.php @@ -0,0 +1,31 @@ +command("pop_frame"); + } + + /** + * Goes into the iframe to do stuff + * @param string $name + * @param int $timeout + * @return mixed + * @throws \Zumba\GastonJS\Exception\BrowserError + * @throws \Exception + */ + public function pushFrame($name, $timeout = null) { + return $this->command("push_frame", $name, $timeout); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserHeadersTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserHeadersTrait.php new file mode 100644 index 000000000..8300048a8 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserHeadersTrait.php @@ -0,0 +1,53 @@ +command('get_headers'); + } + + /** + * Given an array of headers, set such headers for the requests, removing all others + * @param array $headers + * @return mixed + */ + public function setHeaders($headers) { + return $this->command('set_headers', $headers); + } + + /** + * Adds headers to current page overriding the existing ones for the next requests + * @param $headers + * @return mixed + */ + public function addHeaders($headers) { + return $this->command('add_headers', $headers); + } + + /** + * Adds a header to the page making it permanent if needed + * @param $header + * @param $permanent + * @return mixed + */ + public function addHeader($header, $permanent = false) { + return $this->command('add_header', $header, $permanent); + } + + /** + * Gets the response headers after a request + * @return mixed + */ + public function responseHeaders() { + return $this->command('response_headers'); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserMouseEventTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserMouseEventTrait.php new file mode 100644 index 000000000..38ec5a60c --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserMouseEventTrait.php @@ -0,0 +1,69 @@ +command('click', $pageId, $elementId); + } + + /** + * Triggers a right click on a page an element + * @param $pageId + * @param $elementId + * @return mixed + */ + public function rightClick($pageId, $elementId) { + return $this->command('right_click', $pageId, $elementId); + } + + /** + * Triggers a double click in a given page and element + * @param $pageId + * @param $elementId + * @return mixed + */ + public function doubleClick($pageId, $elementId) { + return $this->command('double_click', $pageId, $elementId); + } + + /** + * Hovers over an element in a given page + * @param $pageId + * @param $elementId + * @return mixed + */ + public function hover($pageId, $elementId) { + return $this->command('hover', $pageId, $elementId); + } + + /** + * Click on given coordinates, THIS DOES NOT depend on the page, it just clicks on where we are right now + * @param $coordX + * @param $coordY + * @return mixed + */ + public function clickCoordinates($coordX, $coordY) { + return $this->command('click_coordinates', $coordX, $coordY); + } + + /** + * Scrolls the page by a given left and top coordinates + * @param $left + * @param $top + * @return mixed + */ + public function scrollTo($left, $top) { + return $this->command('scroll_to', $left, $top); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserNavigateTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserNavigateTrait.php new file mode 100644 index 000000000..24189af77 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserNavigateTrait.php @@ -0,0 +1,56 @@ +command('visit', $url); + } + + /** + * Gets the current url we are in + * @return mixed + */ + public function currentUrl() { + return $this->command('current_url'); + } + + /** + * Goes back on the browser history if possible + * @return bool + * @throws BrowserError + * @throws \Exception + */ + public function goBack() { + return $this->command('go_back'); + } + + /** + * Goes forward on the browser history if possible + * @return mixed + * @throws BrowserError + * @throws \Exception + */ + public function goForward() { + return $this->command('go_forward'); + } + + /** + * Reloads the current page we are in + */ + public function reload() { + return $this->command('reload'); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserNetworkTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserNetworkTrait.php new file mode 100644 index 000000000..d79d21ec3 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserNetworkTrait.php @@ -0,0 +1,39 @@ +command('network_traffic'); + $requestTraffic = array(); + + if (count($networkTraffic) === 0) { + return null; + } + + foreach ($networkTraffic as $traffic) { + $requestTraffic[] = new Request($traffic["request"], $traffic["responseParts"]); + } + + return $requestTraffic; + } + + /** + * Clear the network traffic data stored on the phantomjs code + * @return mixed + */ + public function clearNetworkTraffic() { + return $this->command('clear_network_traffic'); + } + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserPageElementTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserPageElementTrait.php new file mode 100644 index 000000000..3f998fa9d --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserPageElementTrait.php @@ -0,0 +1,193 @@ +command('find', $method, $selector); + $found["page_id"] = $result["page_id"]; + foreach ($result["ids"] as $id) { + $found["ids"][] = $id; + } + return $found; + } + + /** + * Find elements within a page, method and selector + * @param $pageId + * @param $elementId + * @param $method + * @param $selector + * @return mixed + */ + public function findWithin($pageId, $elementId, $method, $selector) { + return $this->command('find_within', $pageId, $elementId, $method, $selector); + } + + /** + * @param $pageId + * @param $elementId + * @return mixed + */ + public function getParents($pageId, $elementId) { + return $this->command('parents', $pageId, $elementId); + } + + /** + * Returns the text of a given page and element + * @param $pageId + * @param $elementId + * @return mixed + */ + public function allText($pageId, $elementId) { + return $this->command('all_text', $pageId, $elementId); + } + + /** + * Returns the inner or outer html of the given page and element + * @param $pageId + * @param $elementId + * @param $type + * @return mixed + * @throws \Zumba\GastonJS\Exception\BrowserError + * @throws \Exception + */ + public function allHtml($pageId, $elementId, $type = "inner") { + return $this->command('all_html', $pageId, $elementId, $type); + } + + /** + * Returns ONLY the visible text of a given page and element + * @param $pageId + * @param $elementId + * @return mixed + */ + public function visibleText($pageId, $elementId) { + return $this->command('visible_text', $pageId, $elementId); + } + + /** + * Deletes the text of a given page and element + * @param $pageId + * @param $elementId + * @return mixed + */ + public function deleteText($pageId, $elementId) { + return $this->command('delete_text', $pageId, $elementId); + } + + /** + * Gets the tag name of a given element and page + * @param $pageId + * @param $elementId + * @return string + */ + public function tagName($pageId, $elementId) { + return strtolower($this->command('tag_name', $pageId, $elementId)); + } + + /** + * Check if two elements are the same on a give + * @param $pageId + * @param $firstId + * @param $secondId + * @return bool + */ + public function equals($pageId, $firstId, $secondId) { + return $this->command('equals', $pageId, $firstId, $secondId); + } + + /** + * Returns the attributes of an element in a given page + * @param $pageId + * @param $elementId + * @return mixed + */ + public function attributes($pageId, $elementId) { + return $this->command('attributes', $pageId, $elementId); + } + + /** + * Returns the attribute of an element by name in a given page + * @param $pageId + * @param $elementId + * @param $name + * @return mixed + */ + public function attribute($pageId, $elementId, $name) { + return $this->command('attribute', $pageId, $elementId, $name); + } + + /** + * Set an attribute to the given element in the given page + * @param $pageId + * @param $elementId + * @param $name + * @param $value + * @return mixed + * @throws \Zumba\GastonJS\Exception\BrowserError + * @throws \Exception + */ + public function setAttribute($pageId, $elementId, $name, $value) { + return $this->command('set_attribute', $pageId, $elementId, $name, $value); + } + + /** + * Remove an attribute for a given page and element + * @param $pageId + * @param $elementId + * @param $name + * @return mixed + * @throws \Zumba\GastonJS\Exception\BrowserError + * @throws \Exception + */ + public function removeAttribute($pageId, $elementId, $name) { + return $this->command('remove_attribute', $pageId, $elementId, $name); + } + + /** + * Checks if an element is visible or not + * @param $pageId + * @param $elementId + * @return boolean + */ + public function isVisible($pageId, $elementId) { + return $this->command("visible", $pageId, $elementId); + } + + /** + * Sends the order to execute a key event on a given element + * @param $pageId + * @param $elementId + * @param $keyEvent + * @param $key + * @param $modifier + * @return mixed + */ + public function keyEvent($pageId, $elementId, $keyEvent, $key, $modifier) { + return $this->command("key_event", $pageId, $elementId, $keyEvent, $key, $modifier); + } + + /** + * Sends the command to select and option given a value + * @param $pageId + * @param $elementId + * @param $value + * @param bool $multiple + * @return mixed + */ + public function selectOption($pageId, $elementId, $value, $multiple = false) { + return $this->command("select_option", $pageId, $elementId, $value, $multiple); + } + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserPageTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserPageTrait.php new file mode 100644 index 000000000..3d5f9f121 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserPageTrait.php @@ -0,0 +1,59 @@ +command('status_code'); + } + + /** + * Returns the body of the response to a given browser request + * @return mixed + */ + public function getBody() { + return $this->command('body'); + } + + /** + * Returns the source of the current page + * @return mixed + */ + public function getSource() { + return $this->command('source'); + } + + /** + * Gets the current page title + * @return mixed + */ + public function getTitle() { + return $this->command('title'); + } + + /** + * Resize the current page + * @param $width + * @param $height + * @return mixed + */ + public function resize($width, $height) { + return $this->command('resize', $width, $height); + } + + /** + * Resets the page we are in to a clean slate + * @return mixed + */ + public function reset() { + return $this->command('reset'); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserRenderTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserRenderTrait.php new file mode 100644 index 000000000..3aa10aafb --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserRenderTrait.php @@ -0,0 +1,65 @@ +checkRenderOptions($options); + return $this->command('render', $path, $fixedOptions["full"], $fixedOptions["selector"]); + } + + /** + * Renders base64 a page or selection to a file given by path + * @param string $imageFormat (PNG, GIF, JPEG) + * @param array $options + * @return mixed + */ + public function renderBase64($imageFormat, $options = array()) { + $fixedOptions = $this->checkRenderOptions($options); + return $this->command('render_base64', $imageFormat, $fixedOptions["full"], $fixedOptions["selector"]); + } + + /** + * Sets the paper size, useful when saving to PDF + * @param $paperSize + * @return mixed + */ + public function setPaperSize($paperSize) { + return $this->command('set_paper_size', $paperSize); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserScriptTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserScriptTrait.php new file mode 100644 index 000000000..769b86fc5 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserScriptTrait.php @@ -0,0 +1,41 @@ +command('evaluate', $script); + } + + /** + * Executes a script on the browser + * @param $script + * @return mixed + */ + public function execute($script) { + return $this->command('execute', $script); + } + + /** + * Add desired extensions to phantomjs + * @param $extensions + * @return bool + */ + public function extensions($extensions) { + //TODO: add error control for when extensions do not exist physically + foreach ($extensions as $extensionName) { + $this->command('add_extension', $extensionName); + } + return true; + } + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserWindowTrait.php b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserWindowTrait.php new file mode 100644 index 000000000..8647ffcbb --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Browser/BrowserWindowTrait.php @@ -0,0 +1,81 @@ +command('window_handle', $name); + } + + /** + * Returns all the window handles present in the browser + * @return array + */ + public function windowHandles() { + return $this->command('window_handles'); + } + + /** + * Change the browser focus to another window + * @param $windowHandleName + * @return mixed + */ + public function switchToWindow($windowHandleName) { + return $this->command('switch_to_window', $windowHandleName); + } + + /** + * Opens a new window on the browser + * @return mixed + */ + public function openNewWindow() { + return $this->command('open_new_window'); + } + + /** + * Closes a window on the browser by a given handler name + * @param $windowHandleName + * @return mixed + */ + public function closeWindow($windowHandleName) { + return $this->command('close_window', $windowHandleName); + } + + /** + * Gets the current request window name + * @return string + * @throws \Zumba\GastonJS\Exception\BrowserError + * @throws \Exception + */ + public function windowName() { + return $this->command('window_name'); + } + + /** + * Zoom factor for a web page + * @param $zoomFactor + * @return mixed + */ + public function setZoomFactor($zoomFactor) { + return $this->command('set_zoom_factor', $zoomFactor); + } + + /** + * Gets the window size + * @param $windowHandleName + * @return mixed + */ + public function windowSize($windowHandleName) { + return $this->command('window_size', $windowHandleName); + } + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/browser_error.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/browser_error.js new file mode 100644 index 000000000..892333caa --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/browser_error.js @@ -0,0 +1,17 @@ +Poltergeist.BrowserError = (function (_super) { + __extends(BrowserError, _super); + + function BrowserError(message, stack) { + this.message = message; + this.stack = stack; + } + + BrowserError.prototype.name = "Poltergeist.BrowserError"; + + BrowserError.prototype.args = function () { + return [this.message, this.stack]; + }; + + return BrowserError; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/error.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/error.js new file mode 100644 index 000000000..5a6f1f699 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/error.js @@ -0,0 +1,10 @@ +/** + * Poltergeist base error class + */ +Poltergeist.Error = (function () { + function Error() { + } + + return Error; + +})(); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/frame_not_found.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/frame_not_found.js new file mode 100644 index 000000000..d42e87256 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/frame_not_found.js @@ -0,0 +1,16 @@ +Poltergeist.FrameNotFound = (function (_super) { + __extends(FrameNotFound, _super); + + function FrameNotFound(frameName) { + this.frameName = frameName; + } + + FrameNotFound.prototype.name = "Poltergeist.FrameNotFound"; + + FrameNotFound.prototype.args = function () { + return [this.frameName]; + }; + + return FrameNotFound; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/invalid_selector.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/invalid_selector.js new file mode 100644 index 000000000..2ef4ae94d --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/invalid_selector.js @@ -0,0 +1,17 @@ +Poltergeist.InvalidSelector = (function (_super) { + __extends(InvalidSelector, _super); + + function InvalidSelector(method, selector) { + this.method = method; + this.selector = selector; + } + + InvalidSelector.prototype.name = "Poltergeist.InvalidSelector"; + + InvalidSelector.prototype.args = function () { + return [this.method, this.selector]; + }; + + return InvalidSelector; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/javascript_error.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/javascript_error.js new file mode 100644 index 000000000..b8679e4f6 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/javascript_error.js @@ -0,0 +1,16 @@ +Poltergeist.JavascriptError = (function (_super) { + __extends(JavascriptError, _super); + + function JavascriptError(errors) { + this.errors = errors; + } + + JavascriptError.prototype.name = "Poltergeist.JavascriptError"; + + JavascriptError.prototype.args = function () { + return [this.errors]; + }; + + return JavascriptError; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/mouse_event_failed.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/mouse_event_failed.js new file mode 100644 index 000000000..f3d4e854d --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/mouse_event_failed.js @@ -0,0 +1,18 @@ +Poltergeist.MouseEventFailed = (function (_super) { + __extends(MouseEventFailed, _super); + + function MouseEventFailed(eventName, selector, position) { + this.eventName = eventName; + this.selector = selector; + this.position = position; + } + + MouseEventFailed.prototype.name = "Poltergeist.MouseEventFailed"; + + MouseEventFailed.prototype.args = function () { + return [this.eventName, this.selector, this.position]; + }; + + return MouseEventFailed; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/no_such_window_error.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/no_such_window_error.js new file mode 100644 index 000000000..ee1d5ad7a --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/no_such_window_error.js @@ -0,0 +1,17 @@ +Poltergeist.NoSuchWindowError = (function (_super) { + __extends(NoSuchWindowError, _super); + + function NoSuchWindowError() { + _ref2 = NoSuchWindowError.__super__.constructor.apply(this, arguments); + return _ref2; + } + + NoSuchWindowError.prototype.name = "Poltergeist.NoSuchWindowError"; + + NoSuchWindowError.prototype.args = function () { + return []; + }; + + return NoSuchWindowError; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/obsolete_node.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/obsolete_node.js new file mode 100644 index 000000000..758cfd6b2 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/obsolete_node.js @@ -0,0 +1,21 @@ +Poltergeist.ObsoleteNode = (function (_super) { + __extends(ObsoleteNode, _super); + + function ObsoleteNode() { + _ref = ObsoleteNode.__super__.constructor.apply(this, arguments); + return _ref; + } + + ObsoleteNode.prototype.name = "Poltergeist.ObsoleteNode"; + + ObsoleteNode.prototype.args = function () { + return []; + }; + + ObsoleteNode.prototype.toString = function () { + return this.name; + }; + + return ObsoleteNode; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Errors/status_fail_error.js b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/status_fail_error.js new file mode 100644 index 000000000..55f1871b1 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Errors/status_fail_error.js @@ -0,0 +1,17 @@ +Poltergeist.StatusFailError = (function (_super) { + __extends(StatusFailError, _super); + + function StatusFailError() { + _ref1 = StatusFailError.__super__.constructor.apply(this, arguments); + return _ref1; + } + + StatusFailError.prototype.name = "Poltergeist.StatusFailError"; + + StatusFailError.prototype.args = function () { + return []; + }; + + return StatusFailError; + +})(Poltergeist.Error); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Server/server.js b/vendor/jcalderonzumba/gastonjs/src/Client/Server/server.js new file mode 100644 index 000000000..120d1fd68 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Server/server.js @@ -0,0 +1,80 @@ +Poltergeist.Server = (function () { + + /** + * Server constructor + * @param owner + * @param port + * @constructor + */ + function Server(owner, port) { + this.server = require('webserver').create(); + this.port = port; + this.owner = owner; + this.webServer = null; + } + + /** + * Starts the web server + */ + Server.prototype.start = function () { + var self = this; + this.webServer = this.server.listen(this.port, function (request, response) { + self.handleRequest(request, response); + }); + }; + + /** + * Send error back with code and message + * @param response + * @param code + * @param message + * @return {boolean} + */ + Server.prototype.sendError = function (response, code, message) { + response.statusCode = code; + response.setHeader('Content-Type', 'application/json'); + response.write(JSON.stringify(message, null, 4)); + response.close(); + return true; + }; + + + /** + * Send response back to the client + * @param response + * @param data + * @return {boolean} + */ + Server.prototype.send = function (response, data) { + console.log("RESPONSE: " + JSON.stringify(data, null, 4).substr(0, 200)); + + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + response.write(JSON.stringify(data, null, 4)); + response.close(); + return true; + }; + + /** + * Handles a request to the server + * @param request + * @param response + * @return {boolean} + */ + Server.prototype.handleRequest = function (request, response) { + var commandData; + if (request.method !== "POST") { + return this.sendError(response, 405, "Only POST method is allowed in the service"); + } + console.log("REQUEST: " + request.post + "\n"); + try { + commandData = JSON.parse(request.post); + } catch (parseError) { + return this.sendError(response, 400, "JSON data invalid error: " + parseError.message); + } + + return this.owner.serverRunCommand(commandData, response); + }; + + return Server; +})(); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/Tools/inherit.js b/vendor/jcalderonzumba/gastonjs/src/Client/Tools/inherit.js new file mode 100644 index 000000000..a67a75cc6 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/Tools/inherit.js @@ -0,0 +1,28 @@ +var __extends; +/** + * Helper function so objects can inherit from another + * @param child + * @param parent + * @return {Object} + * @private + */ +__extends = function (child, parent) { + var __hasProp; + __hasProp = {}.hasOwnProperty; + for (var key in parent) { + if (parent.hasOwnProperty(key)) { + if (__hasProp.call(parent, key)) { + child[key] = parent[key]; + } + } + } + + function ClassConstructor() { + this.constructor = child; + } + + ClassConstructor.prototype = parent.prototype; + child.prototype = new ClassConstructor(); + child.__super__ = parent.prototype; + return child; +}; diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/agent.js b/vendor/jcalderonzumba/gastonjs/src/Client/agent.js new file mode 100644 index 000000000..606a6c166 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/agent.js @@ -0,0 +1,896 @@ +var PoltergeistAgent; + +PoltergeistAgent = (function () { + function PoltergeistAgent() { + this.elements = []; + this.nodes = {}; + } + + /** + * Executes an external call done from the web page class + * @param name + * @param args + * @return {*} + */ + PoltergeistAgent.prototype.externalCall = function (name, args) { + var error; + try { + return { + value: this[name].apply(this, args) + }; + } catch (_error) { + error = _error; + return { + error: { + message: error.toString(), + stack: error.stack + } + }; + } + }; + + /** + * Object stringifycation + * @param object + * @return {*} + */ + PoltergeistAgent.stringify = function (object) { + var error; + try { + return JSON.stringify(object, function (key, value) { + if (Array.isArray(this[key])) { + return this[key]; + } else { + return value; + } + }); + } catch (_error) { + error = _error; + if (error instanceof TypeError) { + return '"(cyclic structure)"'; + } else { + throw error; + } + } + }; + + /** + * Name speaks for itself + * @return {string} + */ + PoltergeistAgent.prototype.currentUrl = function () { + return encodeURI(decodeURI(window.location.href)); + }; + + /** + * Given a method of selection (xpath or css), a selector and a possible element to search + * tries to find the elements that matches such selection + * @param method + * @param selector + * @param within + * @return {Array} + */ + PoltergeistAgent.prototype.find = function (method, selector, within) { + var elementForXpath, error, i, results, xpath, _i, _len, _results; + if (within == null) { + within = document; + } + try { + if (method === "xpath") { + xpath = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + results = (function () { + var _i, _ref, _results; + _results = []; + for (i = _i = 0, _ref = xpath.snapshotLength; 0 <= _ref ? _i < _ref : _i > _ref; i = 0 <= _ref ? ++_i : --_i) { + _results.push(xpath.snapshotItem(i)); + } + return _results; + })(); + } else { + results = within.querySelectorAll(selector); + } + _results = []; + for (_i = 0, _len = results.length; _i < _len; _i++) { + elementForXpath = results[_i]; + _results.push(this.register(elementForXpath)); + } + return _results; + } catch (_error) { + error = _error; + if (error.code === DOMException.SYNTAX_ERR || error.code === 51) { + throw new PoltergeistAgent.InvalidSelector; + } else { + throw error; + } + } + }; + + /** + * Register the element in the agent + * @param element + * @return {number} + */ + PoltergeistAgent.prototype.register = function (element) { + this.elements.push(element); + return this.elements.length - 1; + }; + + /** + * Gets the size of the document + * @return {{height: number, width: number}} + */ + PoltergeistAgent.prototype.documentSize = function () { + return { + height: document.documentElement.scrollHeight || document.documentElement.clientHeight, + width: document.documentElement.scrollWidth || document.documentElement.clientWidth + }; + }; + + /** + * Gets a Node by a given id + * @param id + * @return {PoltergeistAgent.Node} + */ + PoltergeistAgent.prototype.get = function (id) { + if (typeof this.nodes[id] == "undefined" || this.nodes[id] === null) { + //Let's try now the elements approach + if (typeof this.elements[id] == "undefined" || this.elements[id] === null) { + throw new PoltergeistAgent.ObsoleteNode; + } + return new PoltergeistAgent.Node(this, this.elements[id]); + } + + return this.nodes[id]; + }; + + /** + * Calls a Node agent function from the Node caller via delegates + * @param id + * @param name + * @param args + * @return {*} + */ + PoltergeistAgent.prototype.nodeCall = function (id, name, args) { + var node; + + node = this.get(id); + if (node.isObsolete()) { + throw new PoltergeistAgent.ObsoleteNode; + } + //TODO: add some error control here, we might not be able to call name function + return node[name].apply(node, args); + }; + + PoltergeistAgent.prototype.beforeUpload = function (id) { + return this.get(id).setAttribute('_poltergeist_selected', ''); + }; + + PoltergeistAgent.prototype.afterUpload = function (id) { + return this.get(id).removeAttribute('_poltergeist_selected'); + }; + + PoltergeistAgent.prototype.clearLocalStorage = function () { + //TODO: WTF where is variable... + return localStorage.clear(); + }; + + return PoltergeistAgent; + +})(); + +PoltergeistAgent.ObsoleteNode = (function () { + function ObsoleteNode() { + } + + ObsoleteNode.prototype.toString = function () { + return "PoltergeistAgent.ObsoleteNode"; + }; + + return ObsoleteNode; + +})(); + +PoltergeistAgent.InvalidSelector = (function () { + function InvalidSelector() { + } + + InvalidSelector.prototype.toString = function () { + return "PoltergeistAgent.InvalidSelector"; + }; + + return InvalidSelector; + +})(); + +PoltergeistAgent.Node = (function () { + + Node.EVENTS = { + FOCUS: ['blur', 'focus', 'focusin', 'focusout'], + MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseover', 'mouseout', 'mouseup', 'contextmenu'], + FORM: ['submit'] + }; + + function Node(agent, element) { + this.agent = agent; + this.element = element; + } + + /** + * Give me the node id of the parent of this node + * @return {number} + */ + Node.prototype.parentId = function () { + return this.agent.register(this.element.parentNode); + }; + + /** + * Returns all the node parents ids up to first child of the dom + * @return {Array} + */ + Node.prototype.parentIds = function () { + var ids, parent; + ids = []; + parent = this.element.parentNode; + while (parent !== document) { + ids.push(this.agent.register(parent)); + parent = parent.parentNode; + } + return ids; + }; + + /** + * Finds and returns the node ids that matches the selector within this node + * @param method + * @param selector + * @return {Array} + */ + Node.prototype.find = function (method, selector) { + return this.agent.find(method, selector, this.element); + }; + + /** + * Checks whether the node is obsolete or not + * @return boolean + */ + Node.prototype.isObsolete = function () { + var obsolete; + + obsolete = function (element) { + if (element.parentNode != null) { + if (element.parentNode === document) { + return false; + } else { + return obsolete(element.parentNode); + } + } else { + return true; + } + }; + + return obsolete(this.element); + }; + + Node.prototype.changed = function () { + var event; + event = document.createEvent('HTMLEvents'); + event.initEvent('change', true, false); + return this.element.dispatchEvent(event); + }; + + Node.prototype.input = function () { + var event; + event = document.createEvent('HTMLEvents'); + event.initEvent('input', true, false); + return this.element.dispatchEvent(event); + }; + + Node.prototype.keyupdowned = function (eventName, keyCode) { + var event; + event = document.createEvent('UIEvents'); + event.initEvent(eventName, true, true); + event.keyCode = keyCode; + event.which = keyCode; + event.charCode = 0; + return this.element.dispatchEvent(event); + }; + + Node.prototype.keypressed = function (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) { + var event; + event = document.createEvent('UIEvents'); + event.initEvent('keypress', true, true); + event.window = this.agent.window; + event.altKey = altKey; + event.ctrlKey = ctrlKey; + event.shiftKey = shiftKey; + event.metaKey = metaKey; + event.keyCode = keyCode; + event.charCode = charCode; + event.which = keyCode; + return this.element.dispatchEvent(event); + }; + + /** + * Tells if the node is inside the body of the document and not somewhere else + * @return {boolean} + */ + Node.prototype.insideBody = function () { + return this.element === document.body || document.evaluate('ancestor::body', this.element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue; + }; + + /** + * Returns all text visible or not of the node + * @return {string} + */ + Node.prototype.allText = function () { + return this.element.textContent; + }; + + /** + * Returns the inner html our outer + * @returns {string} + */ + Node.prototype.allHTML = function (type) { + var returnType = type || 'inner'; + + if (returnType === "inner") { + return this.element.innerHTML; + } + + if (returnType === "outer") { + if (this.element.outerHTML) { + return this.element.outerHTML; + } + // polyfill: + var wrapper = document.createElement('div'); + wrapper.appendChild(this.element.cloneNode(true)); + return wrapper.innerHTML; + } + + return ''; + }; + + /** + * If the element is visible then we return the text + * @return {string} + */ + Node.prototype.visibleText = function () { + if (!this.isVisible(null)) { + return null; + } + + if (this.element.nodeName === "TEXTAREA") { + return this.element.textContent; + } + + return this.element.innerText; + }; + + /** + * Deletes the actual text being represented by a selection object from the node's element DOM. + * @return {*} + */ + Node.prototype.deleteText = function () { + var range; + range = document.createRange(); + range.selectNodeContents(this.element); + window.getSelection().removeAllRanges(); + window.getSelection().addRange(range); + return window.getSelection().deleteFromDocument(); + }; + + /** + * Returns all the attributes {name:value} in the element + * @return {{}} + */ + Node.prototype.getAttributes = function () { + var attributes, i, elementAttributes; + + elementAttributes = this.element.attributes; + attributes = {}; + for (i = 0; i < elementAttributes.length; i++) { + attributes[elementAttributes[i].name] = elementAttributes[i].value.replace("\n", "\\n"); + } + + return attributes; + }; + + /** + * Name speaks for it self, returns the value of a given attribute by name + * @param name + * @return {string} + */ + Node.prototype.getAttribute = function (name) { + if (name === 'checked' || name === 'selected' || name === 'multiple') { + return this.element[name]; + } + return this.element.getAttribute(name); + }; + + /** + * Scrolls the current element into the visible area of the browser window + * @return {*} + */ + Node.prototype.scrollIntoView = function () { + return this.element.scrollIntoViewIfNeeded(); + }; + + /** + * Returns the element.value property with special treatment if the element is a select + * @return {*} + */ + Node.prototype.value = function () { + var options, i, values; + + if (this.element.tagName.toLowerCase() === 'select' && this.element.multiple) { + values = []; + options = this.element.children; + for (i = 0; i < options.length; i++) { + if (options[i].selected) { + values.push(options[i].value); + } + } + return values; + } + + return this.element.value; + }; + + /** + * Sets a given value in the element value property by simulation key interaction + * @param value + * @return {*} + */ + Node.prototype.set = function (value) { + var char, keyCode, i, len; + + if (this.element.readOnly) { + return null; + } + + //respect the maxLength property if present + if (this.element.maxLength >= 0) { + value = value.substr(0, this.element.maxLength); + } + + this.element.value = ''; + this.trigger('focus'); + + if (this.element.type === 'number') { + this.element.value = value; + } else { + for (i = 0, len = value.length; i < len; i++) { + char = value[i]; + keyCode = this.characterToKeyCode(char); + this.keyupdowned('keydown', keyCode); + this.element.value += char; + this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0)); + this.keyupdowned('keyup', keyCode); + } + } + + this.changed(); + this.input(); + + return this.trigger('blur'); + }; + + /** + * Is the node multiple + * @return {boolean} + */ + Node.prototype.isMultiple = function () { + return this.element.multiple; + }; + + /** + * Sets the value of an attribute given by name + * @param name + * @param value + * @return {boolean} + */ + Node.prototype.setAttribute = function (name, value) { + if (value === null) { + return this.removeAttribute(name); + } + + this.element.setAttribute(name, value); + return true; + }; + + /** + * Removes and attribute by name + * @param name + * @return {boolean} + */ + Node.prototype.removeAttribute = function (name) { + this.element.removeAttribute(name); + return true; + }; + + /** + * Selects the current node + * @param value + * @return {boolean} + */ + Node.prototype.select = function (value) { + if (value === false && !this.element.parentNode.multiple) { + return false; + } + + this.element.selected = value; + this.changed(); + return true; + }; + + /** + * Selects the radio button that has the defined value + * @param value + * @return {boolean} + */ + Node.prototype.selectRadioValue = function (value) { + if (this.element.value == value) { + this.element.checked = true; + this.trigger('focus'); + this.trigger('click'); + this.changed(); + return true; + } + + var formElements = this.element.form.elements; + var name = this.element.getAttribute('name'); + var element, i; + + var deselectAllRadios = function (elements, radioName) { + var inputRadioElement; + + for (i = 0; i < elements.length; i++) { + inputRadioElement = elements[i]; + if (inputRadioElement.tagName.toLowerCase() == 'input' && inputRadioElement.type.toLowerCase() == 'radio' && inputRadioElement.name == radioName) { + inputRadioElement.checked = false; + } + } + }; + + var radioChange = function (radioElement) { + var radioEvent; + radioEvent = document.createEvent('HTMLEvents'); + radioEvent.initEvent('change', true, false); + return radioElement.dispatchEvent(radioEvent); + }; + + var radioClickEvent = function (radioElement, name) { + var radioEvent; + radioEvent = document.createEvent('MouseEvent'); + radioEvent.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + return radioElement.dispatchEvent(radioEvent); + }; + + if (!name) { + throw new Poltergeist.BrowserError('The radio button does not have the value "' + value + '"'); + } + + for (i = 0; i < formElements.length; i++) { + element = formElements[i]; + if (element.tagName.toLowerCase() == 'input' && element.type.toLowerCase() == 'radio' && element.name === name) { + if (value === element.value) { + deselectAllRadios(formElements, name); + element.checked = true; + radioClickEvent(element, 'click'); + radioChange(element); + return true; + } + } + } + + throw new Poltergeist.BrowserError('The radio group "' + name + '" does not have an option "' + value + '"'); + }; + + /** + * Checks or uncheck a radio option + * @param value + * @return {boolean} + */ + Node.prototype.checked = function (value) { + //TODO: add error control for the checked stuff + this.element.checked = value; + return true; + }; + + /** + * Returns the element tag name as is, no transformations done + * @return {string} + */ + Node.prototype.tagName = function () { + return this.element.tagName; + }; + + /** + * Checks if the element is visible either by itself of because the parents are visible + * @param element + * @return {boolean} + */ + Node.prototype.isVisible = function (element) { + var nodeElement = element || this.element; + + if (window.getComputedStyle(nodeElement).display === 'none') { + return false; + } else if (nodeElement.parentElement) { + return this.isVisible(nodeElement.parentElement); + } else { + return true; + } + }; + + /** + * Is the node disabled for operations with it? + * @return {boolean} + */ + Node.prototype.isDisabled = function () { + return this.element.disabled || this.element.tagName === 'OPTION' && this.element.parentNode.disabled; + }; + + /** + * Does the node contains the selections + * @return {boolean} + */ + Node.prototype.containsSelection = function () { + var selectedNode; + + selectedNode = document.getSelection().focusNode; + if (!selectedNode) { + return false; + } + //this magic number is NODE.TEXT_NODE + if (selectedNode.nodeType === 3) { + selectedNode = selectedNode.parentNode; + } + + return this.element.contains(selectedNode); + }; + + /** + * Returns the offset of the node in relation to the current frame + * @return {{top: number, left: number}} + */ + Node.prototype.frameOffset = function () { + var offset, rect, style, win; + win = window; + offset = { + top: 0, + left: 0 + }; + while (win.frameElement) { + rect = win.frameElement.getClientRects()[0]; + style = win.getComputedStyle(win.frameElement); + win = win.parent; + offset.top += rect.top + parseInt(style.getPropertyValue("padding-top"), 10); + offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10); + } + return offset; + }; + + /** + * Returns the object position in relation to the window + * @return {{top: *, right: *, left: *, bottom: *, width: *, height: *}} + */ + Node.prototype.position = function () { + var frameOffset, pos, rect; + + rect = this.element.getClientRects()[0]; + if (!rect) { + throw new PoltergeistAgent.ObsoleteNode; + } + + frameOffset = this.frameOffset(); + pos = { + top: rect.top + frameOffset.top, + right: rect.right + frameOffset.left, + left: rect.left + frameOffset.left, + bottom: rect.bottom + frameOffset.top, + width: rect.width, + height: rect.height + }; + + return pos; + }; + + /** + * Triggers a DOM event related to the node element + * @param name + * @return {boolean} + */ + Node.prototype.trigger = function (name) { + var event; + if (Node.EVENTS.MOUSE.indexOf(name) !== -1) { + event = document.createEvent('MouseEvent'); + event.initMouseEvent(name, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + } else if (Node.EVENTS.FOCUS.indexOf(name) !== -1) { + event = this.obtainEvent(name); + } else if (Node.EVENTS.FORM.indexOf(name) !== -1) { + event = this.obtainEvent(name); + } else { + throw "Unknown event"; + } + return this.element.dispatchEvent(event); + }; + + /** + * Creates a generic HTMLEvent to be use in the node element + * @param name + * @return {Event} + */ + Node.prototype.obtainEvent = function (name) { + var event; + event = document.createEvent('HTMLEvents'); + event.initEvent(name, true, true); + return event; + }; + + /** + * Does a check to see if the coordinates given + * match the node element or some of the parents chain + * @param x + * @param y + * @return {*} + */ + Node.prototype.mouseEventTest = function (x, y) { + var elementForXpath, frameOffset, origEl; + + frameOffset = this.frameOffset(); + x -= frameOffset.left; + y -= frameOffset.top; + + elementForXpath = origEl = document.elementFromPoint(x, y); + while (elementForXpath) { + if (elementForXpath === this.element) { + return { + status: 'success' + }; + } else { + elementForXpath = elementForXpath.parentNode; + } + } + + return { + status: 'failure', + selector: origEl && this.getSelector(origEl) + }; + }; + + /** + * Returns the node selector in CSS style (NO xpath) + * @param elementForXpath + * @return {string} + */ + Node.prototype.getSelector = function (elementForXpath) { + var className, selector, i, len, classNames; + + selector = elementForXpath.tagName !== 'HTML' ? this.getSelector(elementForXpath.parentNode) + ' ' : ''; + selector += elementForXpath.tagName.toLowerCase(); + + if (elementForXpath.id) { + selector += "#" + elementForXpath.id; + } + + classNames = elementForXpath.classList; + for (i = 0, len = classNames.length; i < len; i++) { + className = classNames[i]; + selector += "." + className; + } + + return selector; + }; + + /** + * Returns the key code that represents the character + * @param character + * @return {number} + */ + Node.prototype.characterToKeyCode = function (character) { + var code, specialKeys; + code = character.toUpperCase().charCodeAt(0); + specialKeys = { + 96: 192, + 45: 189, + 61: 187, + 91: 219, + 93: 221, + 92: 220, + 59: 186, + 39: 222, + 44: 188, + 46: 190, + 47: 191, + 127: 46, + 126: 192, + 33: 49, + 64: 50, + 35: 51, + 36: 52, + 37: 53, + 94: 54, + 38: 55, + 42: 56, + 40: 57, + 41: 48, + 95: 189, + 43: 187, + 123: 219, + 125: 221, + 124: 220, + 58: 186, + 34: 222, + 60: 188, + 62: 190, + 63: 191 + }; + return specialKeys[code] || code; + }; + + /** + * Checks if one element is equal to other given by its node id + * @param other_id + * @return {boolean} + */ + Node.prototype.isDOMEqual = function (other_id) { + return this.element === this.agent.get(other_id).element; + }; + + /** + * The following function allows one to pass an element and an XML document to find a unique string XPath expression leading back to that element. + * @param element + * @return {string} + */ + Node.prototype.getXPathForElement = function (element) { + var elementForXpath = element || this.element; + var xpath = ''; + var pos, tempitem2; + + while (elementForXpath !== document.documentElement) { + pos = 0; + tempitem2 = elementForXpath; + while (tempitem2) { + if (tempitem2.nodeType === 1 && tempitem2.nodeName === elementForXpath.nodeName) { // If it is ELEMENT_NODE of the same name + pos += 1; + } + tempitem2 = tempitem2.previousSibling; + } + + xpath = "*[name()='" + elementForXpath.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "'][" + pos + ']' + '/' + xpath; + + elementForXpath = elementForXpath.parentNode; + } + + xpath = '/*' + "[name()='" + document.documentElement.nodeName + "' and namespace-uri()='" + (elementForXpath.namespaceURI === null ? '' : elementForXpath.namespaceURI) + "']" + '/' + xpath; + xpath = xpath.replace(/\/$/, ''); + return xpath; + }; + + /** + * Deselect all the options for this element + */ + Node.prototype.deselectAllOptions = function () { + //TODO: error control when the node is not a select node + var i, l = this.element.options.length; + for (i = 0; i < l; i++) { + this.element.options[i].selected = false; + } + }; + + return Node; + +})(); + +window.__poltergeist = new PoltergeistAgent; + +document.addEventListener('DOMContentLoaded', function () { + return console.log('__DOMContentLoaded'); +}); + +window.confirm = function (message) { + return true; +}; + +window.prompt = function (message, _default) { + return _default || null; +}; diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/browser.js b/vendor/jcalderonzumba/gastonjs/src/Client/browser.js new file mode 100644 index 000000000..df667fbac --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/browser.js @@ -0,0 +1,1293 @@ +var __indexOf = [].indexOf || function (item) { + for (var i = 0, l = this.length; i < l; i++) { + if (i in this && this[i] === item) return i; + } + return -1; + }; + +var xpathStringLiteral = function (s) { + if (s.indexOf('"') === -1) + return '"' + s + '"'; + if (s.indexOf("'") === -1) + return "'" + s + "'"; + return 'concat("' + s.replace(/"/g, '",\'"\',"') + '")'; +}; + +Poltergeist.Browser = (function () { + /** + * Creates the "browser" inside phantomjs + * @param owner + * @param width + * @param height + * @constructor + */ + function Browser(owner, width, height) { + this.owner = owner; + this.width = width || 1024; + this.height = height || 768; + this.pages = []; + this.js_errors = true; + this._debug = false; + this._counter = 0; + this.resetPage(); + } + + /** + * Resets the browser to a clean slate + * @return {Function} + */ + Browser.prototype.resetPage = function () { + var _ref; + var self = this; + + _ref = [0, []]; + this._counter = _ref[0]; + this.pages = _ref[1]; + + if (this.page != null) { + if (!this.page.closed) { + if (this.page.currentUrl() !== 'about:blank') { + this.page.clearLocalStorage(); + } + this.page.release(); + } + phantom.clearCookies(); + } + + this.page = this.currentPage = new Poltergeist.WebPage; + this.page.setViewportSize({ + width: this.width, + height: this.height + }); + this.page.handle = "" + (this._counter++); + this.pages.push(this.page); + + return this.page.onPageCreated = function (newPage) { + var page; + page = new Poltergeist.WebPage(newPage); + page.handle = "" + (self._counter++); + return self.pages.push(page); + }; + }; + + /** + * Given a page handle id, tries to get it from the browser page list + * @param handle + * @return {WebPage} + */ + Browser.prototype.getPageByHandle = function (handle) { + var filteredPages; + + //TODO: perhaps we should throw a PageNotFoundByHandle or something like that.. + if (handle === null || typeof handle == "undefined") { + return null; + } + + filteredPages = this.pages.filter(function (p) { + return !p.closed && p.handle === handle; + }); + + if (filteredPages.length === 1) { + return filteredPages[0]; + } + + return null; + }; + + /** + * Sends a debug message to the console + * @param message + * @return {*} + */ + Browser.prototype.debug = function (message) { + if (this._debug) { + return console.log("poltergeist [" + (new Date().getTime()) + "] " + message); + } + }; + + /** + * Given a page_id and id, gets if possible the node in such page + * @param page_id + * @param id + * @return {Poltergeist.Node} + */ + Browser.prototype.node = function (page_id, id) { + if (this.currentPage.id === page_id) { + return this.currentPage.get(id); + } else { + throw new Poltergeist.ObsoleteNode; + } + }; + + /** + * Returns the frameUrl related to the frame given by name + * @param frame_name + * @return {*} + */ + Browser.prototype.frameUrl = function (frame_name) { + return this.currentPage.frameUrl(frame_name); + }; + + /** + * This method defines the rectangular area of the web page to be rasterized when render is invoked. + * If no clipping rectangle is set, render will process the entire web page. + * @param full + * @param selector + * @return {*} + */ + Browser.prototype.set_clip_rect = function (full, selector) { + var dimensions, clipDocument, rect, clipViewport; + + dimensions = this.currentPage.validatedDimensions(); + clipDocument = dimensions.document; + clipViewport = dimensions.viewport; + + if (full) { + rect = { + left: 0, + top: 0, + width: clipDocument.width, + height: clipDocument.height + }; + } else { + if (selector != null) { + rect = this.currentPage.elementBounds(selector); + } else { + rect = { + left: 0, + top: 0, + width: clipViewport.width, + height: clipViewport.height + }; + } + } + + this.currentPage.setClipRect(rect); + return dimensions; + }; + + /** + * Kill the browser, i.e kill phantomjs current process + * @return {int} + */ + Browser.prototype.exit = function () { + return phantom.exit(0); + }; + + /** + * Do nothing + */ + Browser.prototype.noop = function () { + }; + + /** + * Throws a new Object error + */ + Browser.prototype.browser_error = function () { + throw new Error('zomg'); + }; + + /** + * Visits a page and load its content + * @param serverResponse + * @param url + * @return {*} + */ + Browser.prototype.visit = function (serverResponse, url) { + var prevUrl; + var self = this; + this.currentPage.state = 'loading'; + prevUrl = this.currentPage.source === null ? 'about:blank' : this.currentPage.currentUrl(); + this.currentPage.open(url); + if (/#/.test(url) && prevUrl.split('#')[0] === url.split('#')[0]) { + this.currentPage.state = 'default'; + return this.serverSendResponse({ + status: 'success' + }, serverResponse); + } else { + return this.currentPage.waitState('default', function () { + if (self.currentPage.statusCode === null && self.currentPage.status === 'fail') { + return self.owner.serverSendError(new Poltergeist.StatusFailError, serverResponse); + } else { + return self.serverSendResponse({ + status: self.currentPage.status + }, serverResponse); + } + }); + } + }; + + /** + * Puts the control of the browser inside the IFRAME given by name + * @param serverResponse + * @param name + * @param timeout + * @return {*} + */ + Browser.prototype.push_frame = function (serverResponse, name, timeout) { + var _ref; + var self = this; + + if (timeout == null) { + timeout = new Date().getTime() + 2000; + } + + //TODO: WTF, else if after a if with return COMMON + if (_ref = this.frameUrl(name), __indexOf.call(this.currentPage.blockedUrls(), _ref) >= 0) { + return this.serverSendResponse(true, serverResponse); + } else if (this.currentPage.pushFrame(name)) { + if (this.currentPage.currentUrl() === 'about:blank') { + this.currentPage.state = 'awaiting_frame_load'; + return this.currentPage.waitState('default', function () { + return self.serverSendResponse(true, serverResponse); + }); + } else { + return this.serverSendResponse(true, serverResponse); + } + } else { + if (new Date().getTime() < timeout) { + return setTimeout((function () { + return self.push_frame(serverResponse, name, timeout); + }), 50); + } else { + return this.owner.serverSendError(new Poltergeist.FrameNotFound(name), serverResponse); + } + } + }; + + /** + * Injects a javascript into the current page + * @param serverResponse + * @param extension + * @return {*} + */ + Browser.prototype.add_extension = function (serverResponse, extension) { + //TODO: error control when the injection was not possible + this.currentPage.injectExtension(extension); + return this.serverSendResponse('success', serverResponse); + }; + + /** + * Returns the url we are currently in + * @param serverResponse + * @return {*} + */ + Browser.prototype.current_url = function (serverResponse) { + return this.serverSendResponse(this.currentPage.currentUrl(), serverResponse); + }; + + /** + * Returns the current page window name + * @param serverResponse + * @returns {*} + */ + Browser.prototype.window_name = function (serverResponse) { + return this.serverSendResponse(this.currentPage.windowName(), serverResponse); + }; + + /** + * Returns the status code associated to the page + * @param serverResponse + * @return {*} + */ + Browser.prototype.status_code = function (serverResponse) { + if (this.currentPage.statusCode === undefined || this.currentPage.statusCode === null) { + return this.owner.serverSendError(new Poltergeist.StatusFailError("status_code_error"), serverResponse); + } + return this.serverSendResponse(this.currentPage.statusCode, serverResponse); + }; + + /** + * Returns the source code of the active frame, useful for when inside an IFRAME + * @param serverResponse + * @return {*} + */ + Browser.prototype.body = function (serverResponse) { + return this.serverSendResponse(this.currentPage.content(), serverResponse); + }; + + /** + * Returns the source code of the page all the html + * @param serverResponse + * @return {*} + */ + Browser.prototype.source = function (serverResponse) { + return this.serverSendResponse(this.currentPage.source, serverResponse); + }; + + /** + * Returns the current page title + * @param serverResponse + * @return {*} + */ + Browser.prototype.title = function (serverResponse) { + return this.serverSendResponse(this.currentPage.title(), serverResponse); + }; + + /** + * Finds the elements that match a method of selection and a selector + * @param serverResponse + * @param method + * @param selector + * @return {*} + */ + Browser.prototype.find = function (serverResponse, method, selector) { + return this.serverSendResponse({ + page_id: this.currentPage.id, + ids: this.currentPage.find(method, selector) + }, serverResponse); + }; + + /** + * Find elements within a given element + * @param serverResponse + * @param page_id + * @param id + * @param method + * @param selector + * @return {*} + */ + Browser.prototype.find_within = function (serverResponse, page_id, id, method, selector) { + return this.serverSendResponse(this.node(page_id, id).find(method, selector), serverResponse); + }; + + /** + * Returns ALL the text, visible and not visible from the given element + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.all_text = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).allText(), serverResponse); + }; + + /** + * Returns the inner or outer html of a given id + * @param serverResponse + * @param page_id + * @param id + * @param type + * @returns Object + */ + Browser.prototype.all_html = function (serverResponse, page_id, id, type) { + return this.serverSendResponse(this.node(page_id, id).allHTML(type), serverResponse); + }; + + /** + * Returns only the visible text in a given element + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.visible_text = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).visibleText(), serverResponse); + }; + + /** + * Deletes the text in a given element leaving it empty + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.delete_text = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).deleteText(), serverResponse); + }; + + /** + * Gets the value of a given attribute in an element + * @param serverResponse + * @param page_id + * @param id + * @param name + * @return {*} + */ + Browser.prototype.attribute = function (serverResponse, page_id, id, name) { + return this.serverSendResponse(this.node(page_id, id).getAttribute(name), serverResponse); + }; + + /** + * Allows the possibility to set an attribute on a given element + * @param serverResponse + * @param page_id + * @param id + * @param name + * @param value + * @returns {*} + */ + Browser.prototype.set_attribute = function (serverResponse, page_id, id, name, value) { + return this.serverSendResponse(this.node(page_id, id).setAttribute(name, value), serverResponse); + }; + + /** + * Allows the possibility to remove an attribute on a given element + * @param serverResponse + * @param page_id + * @param id + * @param name + * @returns {*} + */ + Browser.prototype.remove_attribute = function (serverResponse, page_id, id, name) { + return this.serverSendResponse(this.node(page_id, id).removeAttribute(name), serverResponse); + }; + + /** + * Returns all the attributes of a given element + * @param serverResponse + * @param page_id + * @param id + * @param name + * @return {*} + */ + Browser.prototype.attributes = function (serverResponse, page_id, id, name) { + return this.serverSendResponse(this.node(page_id, id).getAttributes(), serverResponse); + }; + + /** + * Returns all the way to the document level the parents of a given element + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.parents = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).parentIds(), serverResponse); + }; + + /** + * Returns the element.value of an element given by its page and id + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.value = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).value(), serverResponse); + }; + + /** + * Sets the element.value of an element by the given value + * @param serverResponse + * @param page_id + * @param id + * @param value + * @return {*} + */ + Browser.prototype.set = function (serverResponse, page_id, id, value) { + this.node(page_id, id).set(value); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Uploads a file to an input file element + * @param serverResponse + * @param page_id + * @param id + * @param file_path + * @return {*} + */ + Browser.prototype.select_file = function (serverResponse, page_id, id, file_path) { + var node = this.node(page_id, id); + + this.currentPage.beforeUpload(node.id); + this.currentPage.uploadFile('[_poltergeist_selected]', file_path); + this.currentPage.afterUpload(node.id); + + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sets a value to the selected element (to be used in select elements) + * @param serverResponse + * @param page_id + * @param id + * @param value + * @return {*} + */ + Browser.prototype.select = function (serverResponse, page_id, id, value) { + return this.serverSendResponse(this.node(page_id, id).select(value), serverResponse); + }; + + /** + * Selects an option with the given value + * @param serverResponse + * @param page_id + * @param id + * @param value + * @param multiple + * @return {*} + */ + Browser.prototype.select_option = function (serverResponse, page_id, id, value, multiple) { + return this.serverSendResponse(this.node(page_id, id).select_option(value, multiple), serverResponse); + }; + + /** + * + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.tag_name = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).tagName(), serverResponse); + }; + + + /** + * Tells if an element is visible or not + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.visible = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).isVisible(), serverResponse); + }; + + /** + * Tells if an element is disabled + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.disabled = function (serverResponse, page_id, id) { + return this.serverSendResponse(this.node(page_id, id).isDisabled(), serverResponse); + }; + + /** + * Evaluates a javascript and returns the outcome to the client + * This will be JSON response so your script better be returning objects that can be used + * in JSON.stringify + * @param serverResponse + * @param script + * @return {*} + */ + Browser.prototype.evaluate = function (serverResponse, script) { + return this.serverSendResponse(this.currentPage.evaluate("function() { return " + script + " }"), serverResponse); + }; + + /** + * Executes a javascript and goes back to the client with true if there were no errors + * @param serverResponse + * @param script + * @return {*} + */ + Browser.prototype.execute = function (serverResponse, script) { + this.currentPage.execute("function() { " + script + " }"); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * If inside a frame then we will go back to the parent + * Not defined behaviour if you pop and are not inside an iframe + * @param serverResponse + * @return {*} + */ + Browser.prototype.pop_frame = function (serverResponse) { + return this.serverSendResponse(this.currentPage.popFrame(), serverResponse); + }; + + /** + * Gets the window handle id by a given window name + * @param serverResponse + * @param name + * @return {*} + */ + Browser.prototype.window_handle = function (serverResponse, name) { + var handle, pageByWindowName; + + if (name === null || typeof name == "undefined" || name.length === 0) { + return this.serverSendResponse(this.currentPage.handle, serverResponse); + } + + handle = null; + + //Lets search the handle by the given window name + var filteredPages = this.pages.filter(function (p) { + return !p.closed && p.windowName() === name; + }); + + //A bit of error control is always good + if (Array.isArray(filteredPages) && filteredPages.length >= 1) { + pageByWindowName = filteredPages[0]; + } else { + pageByWindowName = null; + } + + if (pageByWindowName !== null && typeof pageByWindowName != "undefined") { + handle = pageByWindowName.handle; + } + + return this.serverSendResponse(handle, serverResponse); + }; + + /** + * Returns all the window handles of opened windows + * @param serverResponse + * @return {*} + */ + Browser.prototype.window_handles = function (serverResponse) { + var handles, filteredPages; + + filteredPages = this.pages.filter(function (p) { + return !p.closed; + }); + + if (filteredPages.length > 0) { + handles = filteredPages.map(function (p) { + return p.handle; + }); + if (handles.length === 0) { + handles = null; + } + } else { + handles = null; + } + + return this.serverSendResponse(handles, serverResponse); + }; + + /** + * Tries to switch to a window given by the handle id + * @param serverResponse + * @param handle + * @return {*} + */ + Browser.prototype.switch_to_window = function (serverResponse, handle) { + var page; + var self = this; + + page = this.getPageByHandle(handle); + if (page === null || typeof page == "undefined") { + throw new Poltergeist.NoSuchWindowError; + } + + if (page !== this.currentPage) { + return page.waitState('default', function () { + self.currentPage = page; + return self.serverSendResponse(true, serverResponse); + }); + } + + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Opens a new window where we can do stuff + * @param serverResponse + * @return {*} + */ + Browser.prototype.open_new_window = function (serverResponse) { + return this.execute(serverResponse, 'window.open()'); + }; + + /** + * Closes the window given by handle name if possible + * @param serverResponse + * @param handle + * @return {*} + */ + Browser.prototype.close_window = function (serverResponse, handle) { + var page; + + page = this.getPageByHandle(handle); + if (page === null || typeof page == "undefined") { + //TODO: should we throw error since we actually could not find the window? + return this.serverSendResponse(false, serverResponse); + } + + //TODO: we have to add some control here to actually asses that the release has been done + page.release(); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Generic mouse event on an element + * @param serverResponse + * @param page_id + * @param id + * @param name + * @return {number} + */ + Browser.prototype.mouse_event = function (serverResponse, page_id, id, name) { + var node; + var self = this; + node = this.node(page_id, id); + this.currentPage.state = 'mouse_event'; + this.last_mouse_event = node.mouseEvent(name); + return setTimeout(function () { + if (self.currentPage.state === 'mouse_event') { + self.currentPage.state = 'default'; + return self.serverSendResponse({ + position: self.last_mouse_event + }, serverResponse); + } else { + return self.currentPage.waitState('default', function () { + return self.serverSendResponse({ + position: self.last_mouse_event + }, serverResponse); + }); + } + }, 5); + }; + + /** + * Simple click on the element + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.click = function (serverResponse, page_id, id) { + return this.mouse_event(serverResponse, page_id, id, 'click'); + }; + + /** + * Right click on the element + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.right_click = function (serverResponse, page_id, id) { + return this.mouse_event(serverResponse, page_id, id, 'rightclick'); + }; + + /** + * Double click on the element given by page and id + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.double_click = function (serverResponse, page_id, id) { + return this.mouse_event(serverResponse, page_id, id, 'doubleclick'); + }; + + /** + * Executes a mousemove event on the page and given element + * @param serverResponse + * @param page_id + * @param id + * @return {*} + */ + Browser.prototype.hover = function (serverResponse, page_id, id) { + return this.mouse_event(serverResponse, page_id, id, 'mousemove'); + }; + + /** + * Triggers a mouse click event on the given coordinates + * @param serverResponse + * @param x + * @param y + * @return {*} + */ + Browser.prototype.click_coordinates = function (serverResponse, x, y) { + var response; + + this.currentPage.sendEvent('click', x, y); + response = { + click: { + x: x, + y: y + } + }; + + return this.serverSendResponse(response, serverResponse); + }; + + /** + * Drags one element into another, useful for nice javascript thingies + * @param serverResponse + * @param page_id + * @param id + * @param other_id + * @return {*} + */ + Browser.prototype.drag = function (serverResponse, page_id, id, other_id) { + this.node(page_id, id).dragTo(this.node(page_id, other_id)); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Triggers an event on the given page and element + * @param serverResponse + * @param page_id + * @param id + * @param event + * @return {*} + */ + Browser.prototype.trigger = function (serverResponse, page_id, id, event) { + this.node(page_id, id).trigger(event); + return this.serverSendResponse(event, serverResponse); + }; + + /** + * Checks if two elements are equal on a dom level + * @param serverResponse + * @param page_id + * @param id + * @param other_id + * @return {*} + */ + Browser.prototype.equals = function (serverResponse, page_id, id, other_id) { + return this.serverSendResponse(this.node(page_id, id).isEqual(this.node(page_id, other_id)), serverResponse); + }; + + /** + * Resets the current page to a clean slate + * @param serverResponse + * @return {*} + */ + Browser.prototype.reset = function (serverResponse) { + this.resetPage(); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Scrolls to a position given by the left, top coordinates + * @param serverResponse + * @param left + * @param top + * @return {*} + */ + Browser.prototype.scroll_to = function (serverResponse, left, top) { + this.currentPage.setScrollPosition({ + left: left, + top: top + }); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sends keys to an element simulating as closest as possible what a user would do + * when typing + * @param serverResponse + * @param page_id + * @param id + * @param keys + * @return {*} + */ + Browser.prototype.send_keys = function (serverResponse, page_id, id, keys) { + var key, sequence, target, _i, _len; + target = this.node(page_id, id); + if (!target.containsSelection()) { + target.mouseEvent('click'); + } + for (_i = 0, _len = keys.length; _i < _len; _i++) { + sequence = keys[_i]; + key = sequence.key != null ? this.currentPage.keyCode(sequence.key) : sequence; + this.currentPage.sendEvent('keypress', key); + } + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sends a native phantomjs key event to element + * @param serverResponse + * @param page_id + * @param id + * @param keyEvent + * @param key + * @param modifier + */ + Browser.prototype.key_event = function (serverResponse, page_id, id, keyEvent, key, modifier) { + var keyEventModifierMap; + var keyEventModifier; + var target; + + keyEventModifierMap = { + 'none': 0x0, + 'shift': 0x02000000, + 'ctrl': 0x04000000, + 'alt': 0x08000000, + 'meta': 0x10000000 + }; + keyEventModifier = keyEventModifierMap[modifier]; + + target = this.node(page_id, id); + if (!target.containsSelection()) { + target.mouseEvent('click'); + } + target.page.sendEvent(keyEvent, key, null, null, keyEventModifier); + + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sends the rendered page in a base64 encoding + * @param serverResponse + * @param format + * @param full + * @param selector + * @return {*} + */ + Browser.prototype.render_base64 = function (serverResponse, format, full, selector) { + var encoded_image; + if (selector == null) { + selector = null; + } + this.set_clip_rect(full, selector); + encoded_image = this.currentPage.renderBase64(format); + return this.serverSendResponse(encoded_image, serverResponse); + }; + + /** + * Renders the current page entirely or a given selection + * @param serverResponse + * @param path + * @param full + * @param selector + * @return {*} + */ + Browser.prototype.render = function (serverResponse, path, full, selector) { + var dimensions; + if (selector == null) { + selector = null; + } + dimensions = this.set_clip_rect(full, selector); + this.currentPage.setScrollPosition({ + left: 0, + top: 0 + }); + this.currentPage.render(path); + this.currentPage.setScrollPosition({ + left: dimensions.left, + top: dimensions.top + }); + return this.serverSendResponse(true, serverResponse); + }; + + + /** + * Sets the paper size, useful when printing to PDF + * @param serverResponse + * @param size + * @return {*} + */ + Browser.prototype.set_paper_size = function (serverResponse, size) { + this.currentPage.setPaperSize(size); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sets the zoom factor on the current page + * @param serverResponse + * @param zoom_factor + * @return {*} + */ + Browser.prototype.set_zoom_factor = function (serverResponse, zoom_factor) { + this.currentPage.setZoomFactor(zoom_factor); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Resizes the browser viewport, useful when testing mobile stuff + * @param serverResponse + * @param width + * @param height + * @return {*} + */ + Browser.prototype.resize = function (serverResponse, width, height) { + this.currentPage.setViewportSize({ + width: width, + height: height + }); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Gets the browser viewport size + * Because PhantomJS is headless (nothing is shown) + * viewportSize effectively simulates the size of the window like in a traditional browser. + * @param serverResponse + * @param handle + * @return {*} + */ + Browser.prototype.window_size = function (serverResponse, handle) { + //TODO: add support for window handles + return this.serverSendResponse(this.currentPage.viewportSize(), serverResponse); + }; + + /** + * Returns the network traffic that the current page has generated + * @param serverResponse + * @return {*} + */ + Browser.prototype.network_traffic = function (serverResponse) { + return this.serverSendResponse(this.currentPage.networkTraffic(), serverResponse); + }; + + /** + * Clears the accumulated network_traffic in the current page + * @param serverResponse + * @return {*} + */ + Browser.prototype.clear_network_traffic = function (serverResponse) { + this.currentPage.clearNetworkTraffic(); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Gets the headers of the current page + * @param serverResponse + * @return {*} + */ + Browser.prototype.get_headers = function (serverResponse) { + return this.serverSendResponse(this.currentPage.getCustomHeaders(), serverResponse); + }; + + /** + * Set headers in the browser + * @param serverResponse + * @param headers + * @return {*} + */ + Browser.prototype.set_headers = function (serverResponse, headers) { + if (headers['User-Agent']) { + this.currentPage.setUserAgent(headers['User-Agent']); + } + this.currentPage.setCustomHeaders(headers); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Given an array of headers, adds them to the page + * @param serverResponse + * @param headers + * @return {*} + */ + Browser.prototype.add_headers = function (serverResponse, headers) { + var allHeaders, name, value; + allHeaders = this.currentPage.getCustomHeaders(); + for (name in headers) { + if (headers.hasOwnProperty(name)) { + value = headers[name]; + allHeaders[name] = value; + } + } + return this.set_headers(serverResponse, allHeaders); + }; + + /** + * Adds a header to the page temporary or permanently + * @param serverResponse + * @param header + * @param permanent + * @return {*} + */ + Browser.prototype.add_header = function (serverResponse, header, permanent) { + if (!permanent) { + this.currentPage.addTempHeader(header); + } + return this.add_headers(serverResponse, header); + }; + + + /** + * Sends back the client the response headers sent from the browser when making + * the page request + * @param serverResponse + * @return {*} + */ + Browser.prototype.response_headers = function (serverResponse) { + return this.serverSendResponse(this.currentPage.responseHeaders(), serverResponse); + }; + + /** + * Returns the cookies of the current page being browsed + * @param serverResponse + * @return {*} + */ + Browser.prototype.cookies = function (serverResponse) { + return this.serverSendResponse(this.currentPage.cookies(), serverResponse); + }; + + /** + * Sets a cookie in the browser, the format of the cookies has to be the format it says + * on phantomjs documentation and as such you can set it in other domains, not on the + * current page + * @param serverResponse + * @param cookie + * @return {*} + */ + Browser.prototype.set_cookie = function (serverResponse, cookie) { + return this.serverSendResponse(phantom.addCookie(cookie), serverResponse); + }; + + /** + * Remove a cookie set on the current page + * @param serverResponse + * @param name + * @return {*} + */ + Browser.prototype.remove_cookie = function (serverResponse, name) { + //TODO: add error control to check if the cookie was properly deleted + this.currentPage.deleteCookie(name); + phantom.deleteCookie(name); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Clear the cookies in the browser + * @param serverResponse + * @return {*} + */ + Browser.prototype.clear_cookies = function (serverResponse) { + phantom.clearCookies(); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Enables / Disables the cookies on the browser + * @param serverResponse + * @param flag + * @return {*} + */ + Browser.prototype.cookies_enabled = function (serverResponse, flag) { + phantom.cookiesEnabled = flag; + return this.serverSendResponse(true, serverResponse); + }; + + /** + * US19: DONE + * Sets a basic authentication credential to access a page + * THIS SHOULD BE USED BEFORE accessing a page + * @param serverResponse + * @param user + * @param password + * @return {*} + */ + Browser.prototype.set_http_auth = function (serverResponse, user, password) { + this.currentPage.setHttpAuth(user, password); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sets the flag whether to fail on javascript errors or not. + * @param serverResponse + * @param value + * @return {*} + */ + Browser.prototype.set_js_errors = function (serverResponse, value) { + this.js_errors = value; + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Sets the debug mode to boolean value + * @param serverResponse + * @param value + * @return {*} + */ + Browser.prototype.set_debug = function (serverResponse, value) { + this._debug = value; + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Goes back in the history when possible + * @param serverResponse + * @return {*} + */ + Browser.prototype.go_back = function (serverResponse) { + var self = this; + if (this.currentPage.canGoBack()) { + this.currentPage.state = 'loading'; + this.currentPage.goBack(); + return this.currentPage.waitState('default', function () { + return self.serverSendResponse(true, serverResponse); + }); + } else { + return this.serverSendResponse(false, serverResponse); + } + }; + + /** + * Reloads the page if possible + * @return {*} + */ + Browser.prototype.reload = function (serverResponse) { + var self = this; + this.currentPage.state = 'loading'; + this.currentPage.reload(); + return this.currentPage.waitState('default', function () { + return self.serverSendResponse(true, serverResponse); + }); + }; + + /** + * Goes forward in the browser history if possible + * @param serverResponse + * @return {*} + */ + Browser.prototype.go_forward = function (serverResponse) { + var self = this; + if (this.currentPage.canGoForward()) { + this.currentPage.state = 'loading'; + this.currentPage.goForward(); + return this.currentPage.waitState('default', function () { + return self.serverSendResponse(true, serverResponse); + }); + } else { + return this.serverSendResponse(false, serverResponse); + } + }; + + /** + * Sets the urlBlacklist for the given urls as parameters + * @return {boolean} + */ + Browser.prototype.set_url_blacklist = function (serverResponse, blackList) { + this.currentPage.urlBlacklist = Array.prototype.slice.call(blackList); + return this.serverSendResponse(true, serverResponse); + }; + + /** + * Runs a browser command and returns the response back to the client + * when the command has finished the execution + * @param command + * @param serverResponse + * @return {*} + */ + Browser.prototype.serverRunCommand = function (command, serverResponse) { + var commandData; + var commandArgs; + var commandName; + + commandName = command.name; + commandArgs = command.args; + this.currentPage.state = 'default'; + commandData = [serverResponse].concat(commandArgs); + + if (typeof this[commandName] !== "function") { + //We can not run such command + throw new Poltergeist.Error(); + } + + return this[commandName].apply(this, commandData); + }; + + /** + * Sends a response back to the client who made the request + * @param response + * @param serverResponse + * @return {*} + */ + Browser.prototype.serverSendResponse = function (response, serverResponse) { + var errors; + errors = this.currentPage.errors; + this.currentPage.clearErrors(); + if (errors.length > 0 && this.js_errors) { + return this.owner.serverSendError(new Poltergeist.JavascriptError(errors), serverResponse); + } else { + return this.owner.serverSendResponse(response, serverResponse); + } + }; + + return Browser; + +})(); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/main.js b/vendor/jcalderonzumba/gastonjs/src/Client/main.js new file mode 100644 index 000000000..a8f2ecb6d --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/main.js @@ -0,0 +1,29 @@ +var Poltergeist, system, _ref, _ref1, _ref2; + +//Inheritance tool +phantom.injectJs("" + phantom.libraryPath + "/Tools/inherit.js"); + +//Poltergeist main object +phantom.injectJs("" + phantom.libraryPath + "/poltergeist.js"); + +//Errors that are controller in the poltergeist code +phantom.injectJs("" + phantom.libraryPath + "/Errors/error.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/obsolete_node.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/invalid_selector.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/frame_not_found.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/mouse_event_failed.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/javascript_error.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/browser_error.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/status_fail_error.js"); +phantom.injectJs("" + phantom.libraryPath + "/Errors/no_such_window_error.js"); + +//web server to control the commands +phantom.injectJs("" + phantom.libraryPath + "/Server/server.js"); + +phantom.injectJs("" + phantom.libraryPath + "/web_page.js"); +phantom.injectJs("" + phantom.libraryPath + "/node.js"); +phantom.injectJs("" + phantom.libraryPath + "/browser.js"); + +system = require('system'); + +new Poltergeist(system.args[1], system.args[2], system.args[3]); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/node.js b/vendor/jcalderonzumba/gastonjs/src/Client/node.js new file mode 100644 index 000000000..bdf5baf23 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/node.js @@ -0,0 +1,161 @@ +var __slice = [].slice; + +Poltergeist.Node = (function () { + var name, _fn, _i, _len, _ref; + var xpathStringLiteral; + + Node.DELEGATES = ['allText', 'visibleText', 'getAttribute', 'value', 'set', 'checked', + 'setAttribute', 'isObsolete', 'removeAttribute', 'isMultiple', + 'select', 'tagName', 'find', 'getAttributes', 'isVisible', + 'position', 'trigger', 'input', 'parentId', 'parentIds', 'mouseEventTest', + 'scrollIntoView', 'isDOMEqual', 'isDisabled', 'deleteText', 'selectRadioValue', + 'containsSelection', 'allHTML', 'changed', 'getXPathForElement', 'deselectAllOptions']; + + function Node(page, id) { + this.page = page; + this.id = id; + } + + /** + * Returns the parent Node of this Node + * @return {Poltergeist.Node} + */ + Node.prototype.parent = function () { + return new Poltergeist.Node(this.page, this.parentId()); + }; + + _ref = Node.DELEGATES; + + _fn = function (name) { + return Node.prototype[name] = function () { + var args = []; + if (arguments.length >= 1) { + args = __slice.call(arguments, 0) + } + return this.page.nodeCall(this.id, name, args); + }; + }; + + //Adding all the delegates from the agent Node to this Node + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + name = _ref[_i]; + _fn(name); + } + + xpathStringLiteral = function (s) { + if (s.indexOf('"') === -1) + return '"' + s + '"'; + if (s.indexOf("'") === -1) + return "'" + s + "'"; + return 'concat("' + s.replace(/"/g, '",\'"\',"') + '")'; + }; + + /** + * Gets an x,y position tailored for mouse event actions + * @return {{x, y}} + */ + Node.prototype.mouseEventPosition = function () { + var middle, pos, viewport; + + viewport = this.page.viewportSize(); + pos = this.position(); + middle = function (start, end, size) { + return start + ((Math.min(end, size) - start) / 2); + }; + + return { + x: middle(pos.left, pos.right, viewport.width), + y: middle(pos.top, pos.bottom, viewport.height) + }; + }; + + /** + * Executes a phantomjs native mouse event + * @param name + * @return {{x, y}} + */ + Node.prototype.mouseEvent = function (name) { + var pos, test; + + this.scrollIntoView(); + pos = this.mouseEventPosition(); + test = this.mouseEventTest(pos.x, pos.y); + + if (test.status === 'success') { + if (name === 'rightclick') { + this.page.mouseEvent('click', pos.x, pos.y, 'right'); + this.trigger('contextmenu'); + } else { + this.page.mouseEvent(name, pos.x, pos.y); + } + return pos; + } else { + throw new Poltergeist.MouseEventFailed(name, test.selector, pos); + } + }; + + /** + * Executes a mouse based drag from one node to another + * @param other + * @return {{x, y}} + */ + Node.prototype.dragTo = function (other) { + var otherPosition, position; + + this.scrollIntoView(); + position = this.mouseEventPosition(); + otherPosition = other.mouseEventPosition(); + this.page.mouseEvent('mousedown', position.x, position.y); + return this.page.mouseEvent('mouseup', otherPosition.x, otherPosition.y); + }; + + /** + * Checks if one node is equal to another + * @param other + * @return {boolean} + */ + Node.prototype.isEqual = function (other) { + return this.page === other.page && this.isDOMEqual(other.id); + }; + + + /** + * The value to select + * @param value + * @param multiple + */ + Node.prototype.select_option = function (value, multiple) { + var tagName = this.tagName().toLowerCase(); + + if (tagName === "select") { + var escapedOption = xpathStringLiteral(value); + // The value of an option is the normalized version of its text when it has no value attribute + var optionQuery = ".//option[@value = " + escapedOption + " or (not(@value) and normalize-space(.) = " + escapedOption + ")]"; + var ids = this.find("xpath", optionQuery); + var polterNode = this.page.get(ids[0]); + + if (multiple || !this.getAttribute('multiple')) { + if (!polterNode.getAttribute('selected')) { + polterNode.select(value); + this.trigger('click'); + this.input(); + } + return true; + } + + this.deselectAllOptions(); + polterNode.select(value); + this.trigger('click'); + this.input(); + return true; + } else if (tagName === "input" && this.getAttribute("type").toLowerCase() === "radio") { + return this.selectRadioValue(value); + } + + throw new Poltergeist.BrowserError("The element is not a select or radio input"); + + }; + + return Node; + +}).call(this); diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/poltergeist.js b/vendor/jcalderonzumba/gastonjs/src/Client/poltergeist.js new file mode 100644 index 000000000..20f026769 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/poltergeist.js @@ -0,0 +1,77 @@ +Poltergeist = (function () { + + /** + * The MAIN class of the project + * @param port + * @param width + * @param height + * @constructor + */ + function Poltergeist(port, width, height) { + var self; + this.browser = new Poltergeist.Browser(this, width, height); + + this.commandServer = new Poltergeist.Server(this, port); + this.commandServer.start(); + + self = this; + + phantom.onError = function (message, stack) { + return self.onError(message, stack); + }; + + this.running = false; + } + + /** + * Tries to execute a command send by a client and returns the command response + * or error if something happened + * @param command + * @param serverResponse + * @return {boolean} + */ + Poltergeist.prototype.serverRunCommand = function (command, serverResponse) { + var error; + this.running = true; + try { + return this.browser.serverRunCommand(command, serverResponse); + } catch (_error) { + error = _error; + if (error instanceof Poltergeist.Error) { + return this.serverSendError(error, serverResponse); + } + return this.serverSendError(new Poltergeist.BrowserError(error.toString(), error.stack), serverResponse); + } + }; + + /** + * Sends error back to the client + * @param error + * @param serverResponse + * @return {boolean} + */ + Poltergeist.prototype.serverSendError = function (error, serverResponse) { + var errorObject; + errorObject = { + error: { + name: error.name || 'Generic', + args: error.args && error.args() || [error.toString()] + } + }; + return this.commandServer.sendError(serverResponse, 500, errorObject); + }; + + /** + * Send the response back to the client + * @param response Data to send to the client + * @param serverResponse Phantomjs response object associated to the client request + * @return {boolean} + */ + Poltergeist.prototype.serverSendResponse = function (response, serverResponse) { + return this.commandServer.send(serverResponse, {response: response}); + }; + + return Poltergeist; +})(); + +window.Poltergeist = Poltergeist; diff --git a/vendor/jcalderonzumba/gastonjs/src/Client/web_page.js b/vendor/jcalderonzumba/gastonjs/src/Client/web_page.js new file mode 100644 index 000000000..c275b03b7 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Client/web_page.js @@ -0,0 +1,829 @@ +var __slice = [].slice; +var __indexOf = [].indexOf || function (item) { + for (var i = 0, l = this.length; i < l; i++) { + if (i in this && this[i] === item) return i; + } + return -1; + }; + +Poltergeist.WebPage = (function () { + var command, delegate, commandFunctionBind, delegateFunctionBind, i, j, commandsLength, delegatesRefLength, commandsRef, delegatesRef, + _this = this; + + //Native or not webpage callbacks + WebPage.CALLBACKS = ['onAlert', 'onConsoleMessage', 'onLoadFinished', 'onInitialized', 'onLoadStarted', 'onResourceRequested', + 'onResourceReceived', 'onError', 'onNavigationRequested', 'onUrlChanged', 'onPageCreated', 'onClosing']; + + // Delegates the execution to the phantomjs page native functions but directly available in the WebPage object + WebPage.DELEGATES = ['open', 'sendEvent', 'uploadFile', 'release', 'render', 'renderBase64', 'goBack', 'goForward', 'reload']; + + //Commands to execute on behalf of the browser but on the current page + WebPage.COMMANDS = ['currentUrl', 'find', 'nodeCall', 'documentSize', 'beforeUpload', 'afterUpload', 'clearLocalStorage']; + + WebPage.EXTENSIONS = []; + + function WebPage(nativeWebPage) { + var callback, i, callBacksLength, callBacksRef; + + //Lets create the native phantomjs webpage + if (nativeWebPage === null || typeof nativeWebPage == "undefined") { + this._native = require('webpage').create(); + } else { + this._native = nativeWebPage; + } + + this.id = 0; + this.source = null; + this.closed = false; + this.state = 'default'; + this.urlBlacklist = []; + this.frames = []; + this.errors = []; + this._networkTraffic = {}; + this._tempHeaders = {}; + this._blockedUrls = []; + + callBacksRef = WebPage.CALLBACKS; + for (i = 0, callBacksLength = callBacksRef.length; i < callBacksLength; i++) { + callback = callBacksRef[i]; + this.bindCallback(callback); + } + } + + //Bind the commands we can run from the browser to the current page + commandsRef = WebPage.COMMANDS; + commandFunctionBind = function (command) { + return WebPage.prototype[command] = function () { + var args; + args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; + return this.runCommand(command, args); + }; + }; + for (i = 0, commandsLength = commandsRef.length; i < commandsLength; i++) { + command = commandsRef[i]; + commandFunctionBind(command); + } + + //Delegates bind applications + delegatesRef = WebPage.DELEGATES; + delegateFunctionBind = function (delegate) { + return WebPage.prototype[delegate] = function () { + return this._native[delegate].apply(this._native, arguments); + }; + }; + for (j = 0, delegatesRefLength = delegatesRef.length; j < delegatesRefLength; j++) { + delegate = delegatesRef[j]; + delegateFunctionBind(delegate); + } + + /** + * This callback is invoked after the web page is created but before a URL is loaded. + * The callback may be used to change global objects. + * @return {*} + */ + WebPage.prototype.onInitializedNative = function () { + this.id += 1; + this.source = null; + this.injectAgent(); + this.removeTempHeaders(); + return this.setScrollPosition({ + left: 0, + top: 0 + }); + }; + + /** + * This callback is invoked when the WebPage object is being closed, + * either via page.close in the PhantomJS outer space or via window.close in the page's client-side. + * @return {boolean} + */ + WebPage.prototype.onClosingNative = function () { + this.handle = null; + return this.closed = true; + }; + + /** + * This callback is invoked when there is a JavaScript console message on the web page. + * The callback may accept up to three arguments: the string for the message, the line number, and the source identifier. + * @param message + * @param line + * @param sourceId + * @return {boolean} + */ + WebPage.prototype.onConsoleMessageNative = function (message, line, sourceId) { + if (message === '__DOMContentLoaded') { + this.source = this._native.content; + return false; + } + console.log(message); + return true; + }; + + /** + * This callback is invoked when the page starts the loading. There is no argument passed to the callback. + * @return {number} + */ + WebPage.prototype.onLoadStartedNative = function () { + this.state = 'loading'; + return this.requestId = this.lastRequestId; + }; + + /** + * This callback is invoked when the page finishes the loading. + * It may accept a single argument indicating the page's status: 'success' if no network errors occurred, otherwise 'fail'. + * @param status + * @return {string} + */ + WebPage.prototype.onLoadFinishedNative = function (status) { + this.status = status; + this.state = 'default'; + + if (this.source === null || typeof this.source == "undefined") { + this.source = this._native.content; + } else { + this.source = this._native.content; + } + + return this.source; + }; + + /** + * This callback is invoked when there is a JavaScript execution error. + * It is a good way to catch problems when evaluating a script in the web page context. + * The arguments passed to the callback are the error message and the stack trace [as an Array]. + * @param message + * @param stack + * @return {Number} + */ + WebPage.prototype.onErrorNative = function (message, stack) { + var stackString; + + stackString = message; + stack.forEach(function (frame) { + stackString += "\n"; + stackString += " at " + frame.file + ":" + frame.line; + if (frame["function"] && frame["function"] !== '') { + return stackString += " in " + frame["function"]; + } + }); + + return this.errors.push({ + message: message, + stack: stackString + }); + }; + + /** + * This callback is invoked when the page requests a resource. + * The first argument to the callback is the requestData metadata object. + * The second argument is the networkRequest object itself. + * @param requestData + * @param networkRequest + * @return {*} + */ + WebPage.prototype.onResourceRequestedNative = function (requestData, networkRequest) { + var abort; + + abort = this.urlBlacklist.some(function (blacklistedUrl) { + return requestData.url.indexOf(blacklistedUrl) !== -1; + }); + + if (abort) { + if (this._blockedUrls.indexOf(requestData.url) === -1) { + this._blockedUrls.push(requestData.url); + } + //TODO: check this, as it raises onResourceError + return networkRequest.abort(); + } + + this.lastRequestId = requestData.id; + if (requestData.url === this.redirectURL) { + this.redirectURL = null; + this.requestId = requestData.id; + } + + return this._networkTraffic[requestData.id] = { + request: requestData, + responseParts: [] + }; + }; + + /** + * This callback is invoked when a resource requested by the page is received. + * The only argument to the callback is the response metadata object. + * @param response + * @return {*} + */ + WebPage.prototype.onResourceReceivedNative = function (response) { + var networkTrafficElement; + + if ((networkTrafficElement = this._networkTraffic[response.id]) != null) { + networkTrafficElement.responseParts.push(response); + } + + if (this.requestId === response.id) { + if (response.redirectURL) { + return this.redirectURL = response.redirectURL; + } + + this.statusCode = response.status; + return this._responseHeaders = response.headers; + } + }; + + /** + * Inject the poltergeist agent into the webpage + * @return {Array} + */ + WebPage.prototype.injectAgent = function () { + var extension, isAgentInjected, i, extensionsRefLength, extensionsRef, injectionResults; + + isAgentInjected = this["native"]().evaluate(function () { + return typeof window.__poltergeist; + }); + + if (isAgentInjected === "undefined") { + this["native"]().injectJs("" + phantom.libraryPath + "/agent.js"); + extensionsRef = WebPage.EXTENSIONS; + injectionResults = []; + for (i = 0, extensionsRefLength = extensionsRef.length; i < extensionsRefLength; i++) { + extension = extensionsRef[i]; + injectionResults.push(this["native"]().injectJs(extension)); + } + return injectionResults; + } + }; + + /** + * Injects a Javascript file extension into the + * @param file + * @return {*} + */ + WebPage.prototype.injectExtension = function (file) { + //TODO: add error control, for example, check if file already in the extensions array, check if the file exists, etc. + WebPage.EXTENSIONS.push(file); + return this["native"]().injectJs(file); + }; + + /** + * Returns the native phantomjs webpage object + * @return {*} + */ + WebPage.prototype["native"] = function () { + if (this.closed) { + throw new Poltergeist.NoSuchWindowError; + } + + return this._native; + }; + + /** + * Returns the current page window name + * @return {*} + */ + WebPage.prototype.windowName = function () { + return this["native"]().windowName; + }; + + /** + * Returns the keyCode of a given key as set in the phantomjs values + * @param name + * @return {number} + */ + WebPage.prototype.keyCode = function (name) { + return this["native"]().event.key[name]; + }; + + /** + * Waits for the page to reach a certain state + * @param state + * @param callback + * @return {*} + */ + WebPage.prototype.waitState = function (state, callback) { + var self = this; + if (this.state === state) { + return callback.call(); + } else { + return setTimeout((function () { + return self.waitState(state, callback); + }), 100); + } + }; + + /** + * Sets the browser header related to basic authentication protocol + * @param user + * @param password + * @return {boolean} + */ + WebPage.prototype.setHttpAuth = function (user, password) { + var allHeaders = this.getCustomHeaders(); + + if (user === false || password === false) { + if (allHeaders.hasOwnProperty("Authorization")) { + delete allHeaders["Authorization"]; + } + this.setCustomHeaders(allHeaders); + return true; + } + + var userName = user || ""; + var userPassword = password || ""; + + allHeaders["Authorization"] = "Basic " + btoa(userName + ":" + userPassword); + this.setCustomHeaders(allHeaders); + return true; + }; + + /** + * Returns all the network traffic associated to the rendering of this page + * @return {{}} + */ + WebPage.prototype.networkTraffic = function () { + return this._networkTraffic; + }; + + /** + * Clears all the recorded network traffic related to the current page + * @return {{}} + */ + WebPage.prototype.clearNetworkTraffic = function () { + return this._networkTraffic = {}; + }; + + /** + * Returns the blocked urls that the page will not load + * @return {Array} + */ + WebPage.prototype.blockedUrls = function () { + return this._blockedUrls; + }; + + /** + * Clean all the urls that should not be loaded + * @return {Array} + */ + WebPage.prototype.clearBlockedUrls = function () { + return this._blockedUrls = []; + }; + + /** + * This property stores the content of the web page's currently active frame + * (which may or may not be the main frame), enclosed in an HTML/XML element. + * @return {string} + */ + WebPage.prototype.content = function () { + return this["native"]().frameContent; + }; + + /** + * Returns the current active frame title + * @return {string} + */ + WebPage.prototype.title = function () { + return this["native"]().frameTitle; + }; + + /** + * Returns if possible the frame url of the frame given by name + * @param frameName + * @return {string} + */ + WebPage.prototype.frameUrl = function (frameName) { + var query; + + query = function (frameName) { + var iframeReference; + if ((iframeReference = document.querySelector("iframe[name='" + frameName + "']")) != null) { + return iframeReference.src; + } + return void 0; + }; + + return this.evaluate(query, frameName); + }; + + /** + * Remove the errors caught on the page + * @return {Array} + */ + WebPage.prototype.clearErrors = function () { + return this.errors = []; + }; + + /** + * Returns the response headers associated to this page + * @return {{}} + */ + WebPage.prototype.responseHeaders = function () { + var headers; + headers = {}; + this._responseHeaders.forEach(function (item) { + return headers[item.name] = item.value; + }); + return headers; + }; + + /** + * Get Cookies visible to the current URL (though, for setting, use of page.addCookie is preferred). + * This array will be pre-populated by any existing Cookie data visible to this URL that is stored in the CookieJar, if any. + * @return {*} + */ + WebPage.prototype.cookies = function () { + return this["native"]().cookies; + }; + + /** + * Delete any Cookies visible to the current URL with a 'name' property matching cookieName. + * Returns true if successfully deleted, otherwise false. + * @param name + * @return {*} + */ + WebPage.prototype.deleteCookie = function (name) { + return this["native"]().deleteCookie(name); + }; + + /** + * This property gets the size of the viewport for the layout process. + * @return {*} + */ + WebPage.prototype.viewportSize = function () { + return this["native"]().viewportSize; + }; + + /** + * This property sets the size of the viewport for the layout process. + * @param size + * @return {*} + */ + WebPage.prototype.setViewportSize = function (size) { + return this["native"]().viewportSize = size; + }; + + /** + * This property specifies the scaling factor for the page.render and page.renderBase64 functions. + * @param zoomFactor + * @return {*} + */ + WebPage.prototype.setZoomFactor = function (zoomFactor) { + return this["native"]().zoomFactor = zoomFactor; + }; + + /** + * This property defines the size of the web page when rendered as a PDF. + * See: http://phantomjs.org/api/webpage/property/paper-size.html + * @param size + * @return {*} + */ + WebPage.prototype.setPaperSize = function (size) { + return this["native"]().paperSize = size; + }; + + /** + * This property gets the scroll position of the web page. + * @return {*} + */ + WebPage.prototype.scrollPosition = function () { + return this["native"]().scrollPosition; + }; + + /** + * This property defines the scroll position of the web page. + * @param pos + * @return {*} + */ + WebPage.prototype.setScrollPosition = function (pos) { + return this["native"]().scrollPosition = pos; + }; + + + /** + * This property defines the rectangular area of the web page to be rasterized when page.render is invoked. + * If no clipping rectangle is set, page.render will process the entire web page. + * @return {*} + */ + WebPage.prototype.clipRect = function () { + return this["native"]().clipRect; + }; + + /** + * This property defines the rectangular area of the web page to be rasterized when page.render is invoked. + * If no clipping rectangle is set, page.render will process the entire web page. + * @param rect + * @return {*} + */ + WebPage.prototype.setClipRect = function (rect) { + return this["native"]().clipRect = rect; + }; + + /** + * Returns the size of an element given by a selector and its position relative to the viewport. + * @param selector + * @return {Object} + */ + WebPage.prototype.elementBounds = function (selector) { + return this["native"]().evaluate(function (selector) { + return document.querySelector(selector).getBoundingClientRect(); + }, selector); + }; + + /** + * Defines the user agent sent to server when the web page requests resources. + * @param userAgent + * @return {*} + */ + WebPage.prototype.setUserAgent = function (userAgent) { + return this["native"]().settings.userAgent = userAgent; + }; + + /** + * Returns the additional HTTP request headers that will be sent to the server for EVERY request. + * @return {{}} + */ + WebPage.prototype.getCustomHeaders = function () { + return this["native"]().customHeaders; + }; + + /** + * Gets the additional HTTP request headers that will be sent to the server for EVERY request. + * @param headers + * @return {*} + */ + WebPage.prototype.setCustomHeaders = function (headers) { + return this["native"]().customHeaders = headers; + }; + + /** + * Adds a one time only request header, after being used it will be deleted + * @param header + * @return {Array} + */ + WebPage.prototype.addTempHeader = function (header) { + var name, value, tempHeaderResult; + tempHeaderResult = []; + for (name in header) { + if (header.hasOwnProperty(name)) { + value = header[name]; + tempHeaderResult.push(this._tempHeaders[name] = value); + } + } + return tempHeaderResult; + }; + + /** + * Remove the temporary headers we have set via addTempHeader + * @return {*} + */ + WebPage.prototype.removeTempHeaders = function () { + var allHeaders, name, value, tempHeadersRef; + allHeaders = this.getCustomHeaders(); + tempHeadersRef = this._tempHeaders; + for (name in tempHeadersRef) { + if (tempHeadersRef.hasOwnProperty(name)) { + value = tempHeadersRef[name]; + delete allHeaders[name]; + } + } + + return this.setCustomHeaders(allHeaders); + }; + + /** + * If possible switch to the frame given by name + * @param name + * @return {boolean} + */ + WebPage.prototype.pushFrame = function (name) { + if (this["native"]().switchToFrame(name)) { + this.frames.push(name); + return true; + } + return false; + }; + + /** + * Switch to parent frame, use with caution: + * popFrame assumes you are in frame, pop frame not being in a frame + * leaves unexpected behaviour + * @return {*} + */ + WebPage.prototype.popFrame = function () { + //TODO: add some error control here, some way to check we are in a frame or not + this.frames.pop(); + return this["native"]().switchToParentFrame(); + }; + + /** + * Returns the webpage dimensions + * @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}} + */ + WebPage.prototype.dimensions = function () { + var scroll, viewport; + scroll = this.scrollPosition(); + viewport = this.viewportSize(); + return { + top: scroll.top, + bottom: scroll.top + viewport.height, + left: scroll.left, + right: scroll.left + viewport.width, + viewport: viewport, + document: this.documentSize() + }; + }; + + /** + * Returns webpage dimensions that are valid + * @return {{top: *, bottom: *, left: *, right: *, viewport: *, document: {height: number, width: number}}} + */ + WebPage.prototype.validatedDimensions = function () { + var dimensions, documentDimensions; + + dimensions = this.dimensions(); + documentDimensions = dimensions.document; + + if (dimensions.right > documentDimensions.width) { + dimensions.left = Math.max(0, dimensions.left - (dimensions.right - documentDimensions.width)); + dimensions.right = documentDimensions.width; + } + + if (dimensions.bottom > documentDimensions.height) { + dimensions.top = Math.max(0, dimensions.top - (dimensions.bottom - documentDimensions.height)); + dimensions.bottom = documentDimensions.height; + } + + this.setScrollPosition({ + left: dimensions.left, + top: dimensions.top + }); + + return dimensions; + }; + + /** + * Returns a Poltergeist.Node given by an id + * @param id + * @return {Poltergeist.Node} + */ + WebPage.prototype.get = function (id) { + return new Poltergeist.Node(this, id); + }; + + /** + * Executes a phantomjs mouse event, for more info check: http://phantomjs.org/api/webpage/method/send-event.html + * @param name + * @param x + * @param y + * @param button + * @return {*} + */ + WebPage.prototype.mouseEvent = function (name, x, y, button) { + if (button == null) { + button = 'left'; + } + this.sendEvent('mousemove', x, y); + return this.sendEvent(name, x, y, button); + }; + + /** + * Evaluates a javascript and returns the evaluation of such script + * @return {*} + */ + WebPage.prototype.evaluate = function () { + var args, fn; + fn = arguments[0]; + args = []; + + if (2 <= arguments.length) { + args = __slice.call(arguments, 1); + } + + this.injectAgent(); + return JSON.parse(this.sanitize(this["native"]().evaluate("function() { return PoltergeistAgent.stringify(" + (this.stringifyCall(fn, args)) + ") }"))); + }; + + /** + * Does some string sanitation prior parsing + * @param potentialString + * @return {*} + */ + WebPage.prototype.sanitize = function (potentialString) { + if (typeof potentialString === "string") { + return potentialString.replace("\n", "\\n").replace("\r", "\\r"); + } + + return potentialString; + }; + + /** + * Executes a script into the current page scope + * @param script + * @return {*} + */ + WebPage.prototype.executeScript = function (script) { + return this["native"]().evaluateJavaScript(script); + }; + + /** + * Executes a script via phantomjs evaluation + * @return {*} + */ + WebPage.prototype.execute = function () { + var args, fn; + + fn = arguments[0]; + args = []; + + if (2 <= arguments.length) { + args = __slice.call(arguments, 1); + } + + return this["native"]().evaluate("function() { " + (this.stringifyCall(fn, args)) + " }"); + }; + + /** + * Helper methods to do script evaluation and execution + * @param fn + * @param args + * @return {string} + */ + WebPage.prototype.stringifyCall = function (fn, args) { + if (args.length === 0) { + return "(" + (fn.toString()) + ")()"; + } + + return "(" + (fn.toString()) + ").apply(this, JSON.parse(" + (JSON.stringify(JSON.stringify(args))) + "))"; + }; + + /** + * Binds callbacks to their respective Native implementations + * @param name + * @return {Function} + */ + WebPage.prototype.bindCallback = function (name) { + var self; + self = this; + + return this["native"]()[name] = function () { + var result; + if (self[name + 'Native'] != null) { + result = self[name + 'Native'].apply(self, arguments); + } + if (result !== false && (self[name] != null)) { + return self[name].apply(self, arguments); + } + }; + }; + + /** + * Runs a command delegating to the PoltergeistAgent + * @param name + * @param args + * @return {*} + */ + WebPage.prototype.runCommand = function (name, args) { + var method, result, selector; + + result = this.evaluate(function (name, args) { + return window.__poltergeist.externalCall(name, args); + }, name, args); + + if (result !== null) { + if (result.error != null) { + switch (result.error.message) { + case 'PoltergeistAgent.ObsoleteNode': + throw new Poltergeist.ObsoleteNode; + break; + case 'PoltergeistAgent.InvalidSelector': + method = args[0]; + selector = args[1]; + throw new Poltergeist.InvalidSelector(method, selector); + break; + default: + throw new Poltergeist.BrowserError(result.error.message, result.error.stack); + } + } else { + return result.value; + } + } + }; + + /** + * Tells if we can go back or not + * @return {boolean} + */ + WebPage.prototype.canGoBack = function () { + return this["native"]().canGoBack; + }; + + /** + * Tells if we can go forward or not in the browser history + * @return {boolean} + */ + WebPage.prototype.canGoForward = function () { + return this["native"]().canGoForward; + }; + + return WebPage; + +}).call(this); diff --git a/vendor/jcalderonzumba/gastonjs/src/Cookie.php b/vendor/jcalderonzumba/gastonjs/src/Cookie.php new file mode 100644 index 000000000..d59c0f674 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Cookie.php @@ -0,0 +1,79 @@ +attributes = $attributes; + } + + /** + * Returns the cookie name + * @return string + */ + public function getName() { + return $this->attributes['name']; + } + + /** + * Returns the cookie value + * @return string + */ + public function getValue() { + return urldecode($this->attributes['value']); + } + + /** + * Returns the cookie domain + * @return string + */ + public function getDomain() { + return $this->attributes['domain']; + } + + /** + * Returns the path were the cookie is valid + * @return string + */ + public function getPath() { + return $this->attributes['path']; + } + + /** + * Is a secure cookie? + * @return bool + */ + public function isSecure() { + return isset($this->attributes['secure']); + } + + /** + * Is http only cookie? + * @return bool + */ + public function isHttpOnly() { + return isset($this->attributes['httponly']); + } + + /** + * Returns cookie expiration time + * @return mixed + */ + public function getExpirationTime() { + //TODO: return a \DateTime object + if (isset($this->attributes['expiry'])) { + return $this->attributes['expiry']; + } + return null; + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/BrowserError.php b/vendor/jcalderonzumba/gastonjs/src/Exception/BrowserError.php new file mode 100644 index 000000000..a1c4f4b12 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/BrowserError.php @@ -0,0 +1,44 @@ +message = $this->message(); + } + + /** + * Gets the name of the browser error + * @return string + */ + public function getName() { + return $this->response["error"]["name"]; + } + + /** + * @return JSErrorItem + */ + public function javascriptError() { + //TODO: this need to be check, i don't know yet what comes in response + return new JSErrorItem($this->response["error"]["args"][0], $this->response["error"]["args"][1]); + } + + /** + * Returns error message + * TODO: check how to proper implement if we have exceptions + * @return string + */ + public function message() { + return "There was an error inside the PhantomJS portion of GastonJS.\nThis is probably a bug, so please report it:\n" . $this->javascriptError(); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/ClientError.php b/vendor/jcalderonzumba/gastonjs/src/Exception/ClientError.php new file mode 100644 index 000000000..06be87bc2 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/ClientError.php @@ -0,0 +1,36 @@ +response = $response; + } + + /** + * @return mixed + */ + public function getResponse() { + return $this->response; + } + + /** + * @param mixed $response + */ + public function setResponse($response) { + $this->response = $response; + } + + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/DeadClient.php b/vendor/jcalderonzumba/gastonjs/src/Exception/DeadClient.php new file mode 100644 index 000000000..f4af19306 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/DeadClient.php @@ -0,0 +1,21 @@ +response["args"])); + } + + /** + * @return string + */ + public function message() { + //TODO: check the exception message stuff + return "The frame " . $this->getName() . " was not not found"; + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/InvalidSelector.php b/vendor/jcalderonzumba/gastonjs/src/Exception/InvalidSelector.php new file mode 100644 index 000000000..44fad4bec --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/InvalidSelector.php @@ -0,0 +1,32 @@ +response["error"]["args"][0]; + } + + /** + * Gets the selector related to the method + * @return string + */ + public function getSelector() { + return $this->response["error"]["args"][1]; + } + + /** + * @return string + */ + public function message() { + return "The browser raised a syntax error while trying to evaluate" . $this->getMethod() . " selector " . $this->getSelector(); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/JSErrorItem.php b/vendor/jcalderonzumba/gastonjs/src/Exception/JSErrorItem.php new file mode 100644 index 000000000..2fa205afd --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/JSErrorItem.php @@ -0,0 +1,31 @@ +message = $message; + $this->stack = $stack; + } + + /** + * String representation of the class + * @return string + */ + public function __toString() { + return sprintf("%s\n%s", $this->message, $this->stack); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/JavascriptError.php b/vendor/jcalderonzumba/gastonjs/src/Exception/JavascriptError.php new file mode 100644 index 000000000..309adfb97 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/JavascriptError.php @@ -0,0 +1,48 @@ +message = $this->message(); + } + + /** + * Get the javascript errors found during the use of the phantomjs + * @return array + */ + public function javascriptErrors() { + $jsErrors = array(); + $errors = $this->response["error"]["args"][0]; + foreach ($errors as $error) { + $jsErrors[] = new JSErrorItem($error["message"], $error["stack"]); + } + return $jsErrors; + } + + /** + * Returns the javascript errors found + * @return string + */ + public function message() { + $error = "One or more errors were raised in the Javascript code on the page. + If you don't care about these errors, you can ignore them by + setting js_errors: false in your Poltergeist configuration (see documentation for details)."; + //TODO: add javascript errors + $jsErrors = $this->javascriptErrors(); + foreach($jsErrors as $jsError){ + $error = "$error\n$jsError"; + } + return $error; + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/MouseEventFailed.php b/vendor/jcalderonzumba/gastonjs/src/Exception/MouseEventFailed.php new file mode 100644 index 000000000..abd72d3a4 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/MouseEventFailed.php @@ -0,0 +1,49 @@ +response["args"][0]; + } + + /** + * Selector of the element to act with the mouse + * @return string + */ + public function getSelector() { + return $this->response["args"][1]; + } + + /** + * Returns the position where the click was done + * @return array + */ + public function getPosition() { + $position = array(); + $position[0] = $this->response["args"][1]['x']; + $position[1] = $this->response["args"][2]['y']; + return $position; + } + + /** + * @return string + */ + public function message() { + $name = $this->getName(); + $position = implode(",", $this->getPosition()); + return "Firing a $name at co-ordinates [$position] failed. Poltergeist detected + another element with CSS selector '#{selector}' at this position. + It may be overlapping the element you are trying to interact with. + If you don't care about overlapping elements, try using node.trigger('$name')."; + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/NoSuchWindowError.php b/vendor/jcalderonzumba/gastonjs/src/Exception/NoSuchWindowError.php new file mode 100644 index 000000000..45388d16c --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/NoSuchWindowError.php @@ -0,0 +1,10 @@ +node = $node; + parent::__construct($response); + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/ObsoleteNode.php b/vendor/jcalderonzumba/gastonjs/src/Exception/ObsoleteNode.php new file mode 100644 index 000000000..a0cdb1795 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/ObsoleteNode.php @@ -0,0 +1,29 @@ +message = $this->message(); + } + + /** + * @return string + */ + public function message() { + return "The element you are trying to interact with is either not part of the DOM, or is + not currently visible on the page (perhaps display: none is set). + It's possible the element has been replaced by another element and you meant to interact with + the new element. If so you need to do a new 'find' in order to get a reference to the + new element."; + } +} diff --git a/vendor/jcalderonzumba/gastonjs/src/Exception/StatusFailError.php b/vendor/jcalderonzumba/gastonjs/src/Exception/StatusFailError.php new file mode 100644 index 000000000..fd90eefec --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/Exception/StatusFailError.php @@ -0,0 +1,17 @@ +data = $data; + $this->responseParts = $this->createResponseParts($responseParts); + } + + /** + * Creates an array of Response objects from a given response array + * @param $responseParts + * @return array + */ + protected function createResponseParts($responseParts) { + if ($responseParts === null) { + return array(); + } + $responses = array(); + foreach ($responseParts as $responsePart) { + $responses[] = new Response($responsePart); + } + return $responses; + } + + /** + * @return array + */ + public function getResponseParts() { + return $this->responseParts; + } + + /** + * @param array $responseParts + */ + public function setResponseParts($responseParts) { + $this->responseParts = $responseParts; + } + + /** + * Returns the url where the request is going to be made + * @return string + */ + public function getUrl() { + //TODO: add isset maybe? + return $this->data['url']; + } + + /** + * Returns the request method + * @return string + */ + public function getMethod() { + return $this->data['method']; + } + + /** + * Gets the request headers + * @return array + */ + public function getHeaders() { + //TODO: Check if the data is actually an array, else make it array and see implications + return $this->data['headers']; + } + + /** + * Returns if exists the request time + * @return \DateTime + */ + public function getTime() { + if (isset($this->data['time'])) { + $requestTime = new \DateTime(); + //TODO: fix the microseconds to miliseconds + $requestTime->createFromFormat("Y-m-dTH:i:s.uZ", $this->data["time"]); + return $requestTime; + } + return null; + } + +} diff --git a/vendor/jcalderonzumba/gastonjs/src/NetworkTraffic/Response.php b/vendor/jcalderonzumba/gastonjs/src/NetworkTraffic/Response.php new file mode 100644 index 000000000..37edc4250 --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/src/NetworkTraffic/Response.php @@ -0,0 +1,97 @@ +data = $data; + } + + /** + * Gets Response url + * @return string + */ + public function getUrl() { + return $this->data['url']; + } + + /** + * Gets the response status code + * @return int + */ + public function getStatus() { + return intval($this->data['status']); + } + + /** + * Gets the status text of the response + * @return string + */ + public function getStatusText() { + return $this->data['statusText']; + } + + /** + * Gets the response headers + * @return array + */ + public function getHeaders() { + return $this->data['headers']; + } + + /** + * Get redirect url if response is a redirect + * @return string + */ + public function getRedirectUrl() { + if (isset($this->data['redirectUrl']) && !empty($this->data['redirectUrl'])) { + return $this->data['redirectUrl']; + } + return null; + } + + /** + * Returns the size of the response body + * @return int + */ + public function getBodySize() { + if (isset($this->data['bodySize'])) { + return intval($this->data['bodySize']); + } + return 0; + } + + /** + * Returns the content type of the response + * @return string + */ + public function getContentType() { + if (isset($this->data['contentType'])) { + return $this->data['contentType']; + } + return null; + } + + /** + * Returns if exists the response time + * @return \DateTime + */ + public function getTime() { + if (isset($this->data['time'])) { + $requestTime = new \DateTime(); + //TODO: fix the microseconds to miliseconds + $requestTime->createFromFormat("Y-m-dTH:i:s.uZ", $this->data["time"]); + return $requestTime; + } + return null; + } +} diff --git a/vendor/jcalderonzumba/gastonjs/unit_tests.xml b/vendor/jcalderonzumba/gastonjs/unit_tests.xml new file mode 100644 index 000000000..5473787bf --- /dev/null +++ b/vendor/jcalderonzumba/gastonjs/unit_tests.xml @@ -0,0 +1,25 @@ + + + + + tests/unit + + + + + src + + src/ + + + + diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/.travis.yml b/vendor/jcalderonzumba/mink-phantomjs-driver/.travis.yml new file mode 100644 index 000000000..0dd8e587b --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/.travis.yml @@ -0,0 +1,40 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + fast_finish: true + include: + - php: 5.4 + env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' SYMFONY_DEPRECATIONS_HELPER=weak + - php: 5.6 + env: DEPENDENCIES=dev + allow_failures: + - php: 7.0 + - php: hhvm + +cache: + directories: + - $HOME/.composer/cache/files + +before_install: + - composer self-update + - if [ "$DEPENDENCIES" = "dev" ]; then perl -pi -e 's/^}$/,"minimum-stability":"dev"}/' composer.json; fi; + +install: + - composer update $COMPOSER_FLAGS + +before_script: + - mkdir -p /tmp/jcalderonzumba/phantomjs + +script: + - bin/run-tests.sh + +after_script: + - ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {} + - ps axo pid,command | grep php | grep -v grep | awk '{print $1}' | xargs -I {} kill {} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/CHANGELOG-0.2.md b/vendor/jcalderonzumba/mink-phantomjs-driver/CHANGELOG-0.2.md new file mode 100644 index 000000000..e7482b297 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/CHANGELOG-0.2.md @@ -0,0 +1,7 @@ +CHANGELOG for 0.2.x +=================== +This changelog references the relevant changes (bug and security fixes) done in 0.2 minor versions. + +* 0.2.3 + + * bug #1 set_url_blacklist was not working properly (thanks to reporter) diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/LICENSE b/vendor/jcalderonzumba/mink-phantomjs-driver/LICENSE new file mode 100644 index 000000000..7ba018acd --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Juan Francisco Calderón Zumba + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/README.md b/vendor/jcalderonzumba/mink-phantomjs-driver/README.md new file mode 100644 index 000000000..54a433b11 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/README.md @@ -0,0 +1,61 @@ +Mink PhantomJS Driver +=========================== +[![Build Status](https://travis-ci.org/jcalderonzumba/MinkPhantomJSDriver.svg?branch=master)](https://travis-ci.org/jcalderonzumba/MinkPhantomJSDriver) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jcalderonzumba/MinkPhantomJSDriver/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jcalderonzumba/MinkPhantomJSDriver/?branch=master) +[![Latest Stable Version](https://poser.pugx.org/jcalderonzumba/mink-phantomjs-driver/v/stable)](https://packagist.org/packages/jcalderonzumba/mink-phantomjs-driver) +[![Total Downloads](https://poser.pugx.org/jcalderonzumba/mink-phantomjs-driver/downloads)](https://packagist.org/packages/jcalderonzumba/mink-phantomjs-driver) + +Installation & Compatibility +---------------------------- +You need a working installation of [PhantomJS](http://phantomjs.org/download.html) + +This driver is tested using PhantomJS 1.9.8 but it should work with 1.9.X or latest 2.0.X versions + +This driver supports **PHP 5.4 or greater**, there is NO support for PHP 5.3 + +Use [Composer](https://getcomposer.org/) to install all required PHP dependencies: + +```bash +$ composer require --dev behat/mink jcalderonzumba/mink-phantomjs-driver +``` + +How to use +------------- +Extension configuration (for the moment NONE). +```yml +default: + extensions: + Zumba\PhantomJSExtension: +``` +Driver specific configuration: +```yml +Behat\MinkExtension: +phantomjs: + phantom_server: "http://localhost:8510/api" + template_cache: "/tmp/pjsdrivercache/phantomjs" +``` +PhantomJS browser start: +```bash +phantomjs --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /tmp/gastonjs.log & +``` + +FAQ +--------- + +1. Is this a selenium based driver?: + + **NO**, it has nothing to do with Selenium it's inspired on [Poltergeist](https://github.com/teampoltergeist/poltergeist) + +2. What features does this driver implements? + + **ALL** of the features defined in Mink DriverInterface. maximizeWindow is the only one not implemented since is a headless browser it does not make sense to implement it. + +3. Do i need to modify my selenium based tests? + + If you only use the standard behat driver defined methods then NO, you just have to change your default javascript driver. + + +Copyright +--------- + +Copyright (c) 2015 Juan Francisco Calderon Zumba diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/bin/run-tests.sh b/vendor/jcalderonzumba/mink-phantomjs-driver/bin/run-tests.sh new file mode 100644 index 000000000..60bd21941 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/bin/run-tests.sh @@ -0,0 +1,40 @@ +#!/bin/sh +set -e + +start_browser_api(){ + CURRENT_DIR=$(pwd) + LOCAL_PHANTOMJS="${CURRENT_DIR}/bin/phantomjs" + if [ -f ${LOCAL_PHANTOMJS} ]; then + ${LOCAL_PHANTOMJS} --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 & + else + phantomjs --ssl-protocol=any --ignore-ssl-errors=true vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /dev/null & + fi + sleep 2 +} + +stop_services(){ + ps axo pid,command | grep phantomjs | grep -v grep | awk '{print $1}' | xargs -I {} kill {} + ps axo pid,command | grep php | grep -v grep | grep -v phpstorm | awk '{print $1}' | xargs -I {} kill {} + sleep 2 +} + +star_local_browser(){ + CURRENT_DIR=$(pwd) + cd ${CURRENT_DIR}/vendor/behat/mink/driver-testsuite/web-fixtures + if [ "$TRAVIS" = true ]; then + echo "Starting webserver fox fixtures...." + ~/.phpenv/versions/5.6/bin/php -S 127.0.0.1:6789 > /dev/null 2>&1 & + else + php -S 127.0.0.1:6789 2>&1 >> /dev/null & + fi + sleep 2 +} + +mkdir -p /tmp/jcalderonzumba/phantomjs +stop_services +start_browser_api +star_local_browser +cd ${CURRENT_DIR} +${CURRENT_DIR}/bin/phpunit --configuration integration_tests.xml +stop_services +start_browser_api diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/composer.json b/vendor/jcalderonzumba/mink-phantomjs-driver/composer.json new file mode 100644 index 000000000..31b4f57a4 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/composer.json @@ -0,0 +1,53 @@ +{ + "name": "jcalderonzumba/mink-phantomjs-driver", + "description": "PhantomJS driver for Mink framework", + "keywords": [ + "phantomjs", + "headless", + "javascript", + "ajax", + "testing", + "browser" + ], + "homepage": "http://mink.behat.org/", + "type": "mink-driver", + "license": "MIT", + "authors": [ + { + "name": "Juan Francisco Calderón Zumba", + "email": "juanfcz@gmail.com", + "homepage": "http://github.com/jcalderonzumba" + } + ], + "require": { + "php": ">=5.4", + "behat/mink": "~1.6", + "twig/twig": "~1.8", + "jcalderonzumba/gastonjs": "~1.0" + }, + "require-dev": { + "symfony/process": "~2.3", + "symfony/phpunit-bridge": "~2.7", + "symfony/css-selector": "~2.1", + "phpunit/phpunit": "~4.6", + "silex/silex": "~1.2" + }, + "config": { + "bin-dir": "bin" + }, + "autoload": { + "psr-4": { + "Zumba\\Mink\\Driver\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Behat\\Mink\\Tests\\Driver\\": "tests/integration" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.4.x-dev" + } + } +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/integration_tests.xml b/vendor/jcalderonzumba/mink-phantomjs-driver/integration_tests.xml new file mode 100644 index 000000000..739fc365a --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/integration_tests.xml @@ -0,0 +1,38 @@ + + + + + + tests/integration + vendor/behat/mink/driver-testsuite/tests/Basic/BasicAuthTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/ContentTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/CookieTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/ErrorHandlingTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/IFrameTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/ScreenshotTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/TraversingTest.php + vendor/behat/mink/driver-testsuite/tests/Basic/VisibilityTest.php + vendor/behat/mink/driver-testsuite/tests/Form + vendor/behat/mink/driver-testsuite/tests/Js + + + + + + + + + + + + + + + + + + + ./src/Behat/Mink/Driver + + + diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/BasePhantomJSDriver.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/BasePhantomJSDriver.php new file mode 100644 index 000000000..d962ff526 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/BasePhantomJSDriver.php @@ -0,0 +1,109 @@ +phantomHost = $phantomHost; + $this->browser = new Browser($phantomHost); + $this->templateLoader = new \Twig_Loader_Filesystem(realpath(__DIR__ . '/Resources/Script')); + $this->templateEnv = new \Twig_Environment($this->templateLoader, array('cache' => $this->templateCacheSetup($templateCache), 'strict_variables' => true)); + } + + /** + * Sets up the cache template location for the scripts we are going to create with the driver + * @param $templateCache + * @return string + * @throws DriverException + */ + protected function templateCacheSetup($templateCache) { + $cacheDir = $templateCache; + if ($templateCache === null) { + $cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "jcalderonzumba" . DIRECTORY_SEPARATOR . "phantomjs"; + if (!file_exists($cacheDir)) { + mkdir($cacheDir, 0777, true); + } + } + + if (!file_exists($cacheDir)) { + throw new DriverException("Template cache $cacheDir directory does not exist"); + } + return $cacheDir; + } + + /** + * Helper to find a node element given an xpath + * @param string $xpath + * @param int $max + * @returns int + * @throws DriverException + */ + protected function findElement($xpath, $max = 1) { + $elements = $this->browser->find("xpath", $xpath); + if (!isset($elements["page_id"]) || !isset($elements["ids"]) || count($elements["ids"]) !== $max) { + throw new DriverException("Failed to get elements with given $xpath"); + } + return $elements; + } + + /** + * {@inheritdoc} + * @param Session $session + */ + public function setSession(Session $session) { + $this->session = $session; + } + + /** + * @return Browser + */ + public function getBrowser() { + return $this->browser; + } + + /** + * @return \Twig_Environment + */ + public function getTemplateEnv() { + return $this->templateEnv; + } + + /** + * Returns a javascript script via twig template engine + * @param $templateName + * @param $viewData + * @return string + */ + public function javascriptTemplateRender($templateName, $viewData) { + /** @var $templateEngine \Twig_Environment */ + $templateEngine = $this->getTemplateEnv(); + return $templateEngine->render($templateName, $viewData); + } + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/CookieTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/CookieTrait.php new file mode 100644 index 000000000..327b94885 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/CookieTrait.php @@ -0,0 +1,45 @@ +browser->removeCookie($name); + } + //TODO: set the cookie with domain, not with url, meaning www.aaa.com or .aaa.com + if ($value !== null) { + $urlData = parse_url($this->getCurrentUrl()); + $cookie = array("name" => $name, "value" => $value, "domain" => $urlData["host"]); + $this->browser->setCookie($cookie); + } + } + + /** + * Gets a cookie by its name if exists, else it will return null + * @param string $name + * @return string + */ + public function getCookie($name) { + $cookies = $this->browser->cookies(); + foreach ($cookies as $cookie) { + if ($cookie instanceof Cookie && strcmp($cookie->getName(), $name) === 0) { + return $cookie->getValue(); + } + } + return null; + } + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/FormManipulationTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/FormManipulationTrait.php new file mode 100644 index 000000000..5a94fc762 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/FormManipulationTrait.php @@ -0,0 +1,168 @@ +findElement($xpath, 1); + $javascript = $this->javascriptTemplateRender("get_value.js.twig", array("xpath" => $xpath)); + return $this->browser->evaluate($javascript); + } + + /** + * @param string $xpath + * @param string $value + * @throws DriverException + */ + public function setValue($xpath, $value) { + $this->findElement($xpath, 1); + //This stuff is BECAUSE the way the driver works for setting values when being checkboxes, radios, etc. + if (is_bool($value)) { + $value = $this->boolToString($value); + } + + $javascript = $this->javascriptTemplateRender("set_value.js.twig", array("xpath" => $xpath, "value" => json_encode($value))); + $this->browser->evaluate($javascript); + } + + + /** + * Submits a form given an xpath selector + * @param string $xpath + * @throws DriverException + */ + public function submitForm($xpath) { + $element = $this->findElement($xpath, 1); + $tagName = $this->browser->tagName($element["page_id"], $element["ids"][0]); + if (strcmp(strtolower($tagName), "form") !== 0) { + throw new DriverException("Can not submit something that is not a form"); + } + $this->browser->trigger($element["page_id"], $element["ids"][0], "submit"); + } + + /** + * Helper method needed for twig and javascript stuff + * @param $boolValue + * @return string + */ + protected function boolToString($boolValue) { + if ($boolValue === true) { + return "1"; + } + return "0"; + } + + /** + * Selects an option + * @param string $xpath + * @param string $value + * @param bool $multiple + * @return bool + * @throws DriverException + */ + public function selectOption($xpath, $value, $multiple = false) { + $element = $this->findElement($xpath, 1); + $tagName = strtolower($this->browser->tagName($element["page_id"], $element["ids"][0])); + $attributes = $this->browser->attributes($element["page_id"], $element["ids"][0]); + + if (!in_array($tagName, array("input", "select"))) { + throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath)); + } + + if ($tagName === "input" && $attributes["type"] != "radio") { + throw new DriverException(sprintf('Impossible to select an option on the element with XPath "%s" as it is not a select or radio input', $xpath)); + } + + return $this->browser->selectOption($element["page_id"], $element["ids"][0], $value, $multiple); + } + + /** + * Check control over an input element of radio or checkbox type + * @param $xpath + * @return bool + * @throws DriverException + */ + protected function inputCheckableControl($xpath) { + $element = $this->findElement($xpath, 1); + $tagName = strtolower($this->browser->tagName($element["page_id"], $element["ids"][0])); + $attributes = $this->browser->attributes($element["page_id"], $element["ids"][0]); + if ($tagName != "input") { + throw new DriverException("Can not check when the element is not of the input type"); + } + if (!in_array($attributes["type"], array("checkbox", "radio"))) { + throw new DriverException("Can not check when the element is not checkbox or radio"); + } + return true; + } + + /** + * We click on the checkbox or radio when possible and needed + * @param string $xpath + * @throws DriverException + */ + public function check($xpath) { + $this->inputCheckableControl($xpath); + $javascript = $this->javascriptTemplateRender("check_element.js.twig", array("xpath" => $xpath, "check" => "true")); + $this->browser->evaluate($javascript); + } + + /** + * We click on the checkbox or radio when possible and needed + * @param string $xpath + * @throws DriverException + */ + public function uncheck($xpath) { + $this->inputCheckableControl($xpath); + $javascript = $this->javascriptTemplateRender("check_element.js.twig", array("xpath" => $xpath, "check" => "false")); + $this->browser->evaluate($javascript); + } + + /** + * Checks if the radio or checkbox is checked + * @param string $xpath + * @return bool + * @throws DriverException + */ + public function isChecked($xpath) { + $this->findElement($xpath, 1); + $javascript = $this->javascriptTemplateRender("is_checked.js.twig", array("xpath" => $xpath)); + $checked = $this->browser->evaluate($javascript); + + if ($checked === null) { + throw new DriverException("Can not check when the element is not checkbox or radio"); + } + + return $checked; + } + + /** + * Checks if the option is selected or not + * @param string $xpath + * @return bool + * @throws DriverException + */ + public function isSelected($xpath) { + $elements = $this->findElement($xpath, 1); + $javascript = $this->javascriptTemplateRender("is_selected.js.twig", array("xpath" => $xpath)); + $tagName = $this->browser->tagName($elements["page_id"], $elements["ids"][0]); + if (strcmp(strtolower($tagName), "option") !== 0) { + throw new DriverException("Can not assert on element that is not an option"); + } + + return $this->browser->evaluate($javascript); + } +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/HeadersTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/HeadersTrait.php new file mode 100644 index 000000000..1d6865aca --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/HeadersTrait.php @@ -0,0 +1,40 @@ +browser->responseHeaders(); + } + + /** + * Current request status code response + * @return int + */ + public function getStatusCode() { + return $this->browser->getStatusCode(); + } + + /** + * The name say its all + * @param string $name + * @param string $value + */ + public function setRequestHeader($name, $value) { + $header = array(); + $header[$name] = $value; + //TODO: as a limitation of the driver it self, we will send permanent for the moment + $this->browser->addHeader($header, true); + } + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/JavascriptTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/JavascriptTrait.php new file mode 100644 index 000000000..c5b7d6304 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/JavascriptTrait.php @@ -0,0 +1,49 @@ +browser->execute($script); + } + + /** + * Evaluates a script and returns the result + * @param string $script + * @return mixed + */ + public function evaluateScript($script) { + return $this->browser->evaluate($script); + } + + /** + * Waits some time or until JS condition turns true. + * + * @param integer $timeout timeout in milliseconds + * @param string $condition JS condition + * @return boolean + * @throws DriverException When the operation cannot be done + */ + public function wait($timeout, $condition) { + $start = microtime(true); + $end = $start + $timeout / 1000.0; + do { + $result = $this->browser->evaluate($condition); + usleep(100000); + } while (microtime(true) < $end && !$result); + + return (bool)$result; + } + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/KeyboardTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/KeyboardTrait.php new file mode 100644 index 000000000..2b0c96d20 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/KeyboardTrait.php @@ -0,0 +1,95 @@ +findElement($xpath, 1); + $key = $this->normalizeCharForKeyEvent($char); + $modifier = $this->keyEventModifierControl($modifier); + return $this->browser->keyEvent($element["page_id"], $element["ids"][0], "keydown", $key, $modifier); + } + + /** + * @param string $xpath + * @param string $char + * @param string $modifier + * @throws DriverException + */ + public function keyPress($xpath, $char, $modifier = null) { + $element = $this->findElement($xpath, 1); + $key = $this->normalizeCharForKeyEvent($char); + $modifier = $this->keyEventModifierControl($modifier); + return $this->browser->keyEvent($element["page_id"], $element["ids"][0], "keypress", $key, $modifier); + } + + /** + * Pressed up specific keyboard key. + * + * @param string $xpath + * @param string|integer $char could be either char ('b') or char-code (98) + * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta') + * + * @throws DriverException When the operation cannot be done + */ + public function keyUp($xpath, $char, $modifier = null) { + $this->findElement($xpath, 1); + $element = $this->findElement($xpath, 1); + $key = $this->normalizeCharForKeyEvent($char); + $modifier = $this->keyEventModifierControl($modifier); + return $this->browser->keyEvent($element["page_id"], $element["ids"][0], "keyup", $key, $modifier); + } +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/MouseTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/MouseTrait.php new file mode 100644 index 000000000..c7cd2ebbc --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/MouseTrait.php @@ -0,0 +1,57 @@ +findElement($xpath, 1); + $this->browser->hover($element["page_id"], $element["ids"][0]); + } + + /** + * Clicks if possible on an element given by xpath + * @param string $xpath + * @return mixed + * @throws DriverException + */ + public function click($xpath) { + $elements = $this->findElement($xpath, 1); + $this->browser->click($elements["page_id"], $elements["ids"][0]); + } + + /** + * {@inheritdoc} + */ + /** + * Double click on element found via xpath + * @param string $xpath + * @throws DriverException + */ + public function doubleClick($xpath) { + $elements = $this->findElement($xpath, 1); + $this->browser->doubleClick($elements["page_id"], $elements["ids"][0]); + } + + /** + * Right click on element found via xpath + * @param string $xpath + * @throws DriverException + */ + public function rightClick($xpath) { + $elements = $this->findElement($xpath, 1); + $this->browser->rightClick($elements["page_id"], $elements["ids"][0]); + } + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/NavigationTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/NavigationTrait.php new file mode 100644 index 000000000..88ca42934 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/NavigationTrait.php @@ -0,0 +1,49 @@ +browser->visit($url); + } + + /** + * Gets the current url if any + * @return string + */ + public function getCurrentUrl() { + return $this->browser->currentUrl(); + } + + + /** + * Reloads the page if possible + */ + public function reload() { + $this->browser->reload(); + } + + /** + * Goes forward if possible + */ + public function forward() { + $this->browser->goForward(); + } + + /** + * Goes back if possible + */ + public function back() { + $this->browser->goBack(); + } + + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/PageContentTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/PageContentTrait.php new file mode 100644 index 000000000..7759a0fd7 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/PageContentTrait.php @@ -0,0 +1,72 @@ +browser->getBody(); + } + + /** + * Given xpath, will try to get ALL the text, visible and not visible from such xpath + * @param string $xpath + * @return string + * @throws DriverException + */ + public function getText($xpath) { + $elements = $this->findElement($xpath, 1); + //allText works only with ONE element so it will be the first one and also returns new lines that we will remove + $text = $this->browser->allText($elements["page_id"], $elements["ids"][0]); + $text = trim(str_replace(array("\r", "\r\n", "\n"), ' ', $text)); + $text = preg_replace('/ {2,}/', ' ', $text); + return $text; + } + + /** + * Returns the inner html of a given xpath + * @param string $xpath + * @return string + * @throws DriverException + */ + public function getHtml($xpath) { + $elements = $this->findElement($xpath, 1); + //allText works only with ONE element so it will be the first one + return $this->browser->allHtml($elements["page_id"], $elements["ids"][0], "inner"); + } + + /** + * Gets the outer html of a given xpath + * @param string $xpath + * @return string + * @throws DriverException + */ + public function getOuterHtml($xpath) { + $elements = $this->findElement($xpath, 1); + //allText works only with ONE element so it will be the first one + return $this->browser->allHtml($elements["page_id"], $elements["ids"][0], "outer"); + } + + /** + * Returns the binary representation of the current page we are in + * @throws DriverException + * @return string + */ + public function getScreenshot() { + $options = array("full" => true, "selector" => null); + $b64ScreenShot = $this->browser->renderBase64("JPEG", $options); + if (($binaryScreenShot = base64_decode($b64ScreenShot, true)) === false) { + throw new DriverException("There was a problem while doing the screenshot of the current page"); + } + return $binaryScreenShot; + } +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/PhantomJSDriver.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/PhantomJSDriver.php new file mode 100644 index 000000000..ac251707d --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/PhantomJSDriver.php @@ -0,0 +1,164 @@ +browser->setHttpAuth($user, $password); + } + + /** + * Gets the tag name of a given xpath + * @param string $xpath + * @return string + * @throws DriverException + */ + public function getTagName($xpath) { + $elements = $this->findElement($xpath, 1); + return $this->browser->tagName($elements["page_id"], $elements["ids"][0]); + } + + /** + * Gets the attribute value of a given element and name + * @param string $xpath + * @param string $name + * @return string + * @throws DriverException + */ + public function getAttribute($xpath, $name) { + $elements = $this->findElement($xpath, 1); + return $this->browser->attribute($elements["page_id"], $elements["ids"][0], $name); + } + + /** + * Check if element given by xpath is visible or not + * @param string $xpath + * @return bool + * @throws DriverException + */ + public function isVisible($xpath) { + $elements = $this->findElement($xpath, 1); + return $this->browser->isVisible($elements["page_id"], $elements["ids"][0]); + } + + /** + * Drags one element to another + * @param string $sourceXpath + * @param string $destinationXpath + * @throws DriverException + */ + public function dragTo($sourceXpath, $destinationXpath) { + $sourceElement = $this->findElement($sourceXpath, 1); + $destinationElement = $this->findElement($destinationXpath, 1); + $this->browser->drag($sourceElement["page_id"], $sourceElement["ids"][0], $destinationElement["ids"][0]); + } + + /** + * Upload a file to the browser + * @param string $xpath + * @param string $path + * @throws DriverException + */ + public function attachFile($xpath, $path) { + if (!file_exists($path)) { + throw new DriverException("Wow there the file does not exist, you can not upload it"); + } + + if (($realPath = realpath($path)) === false) { + throw new DriverException("Wow there the file does not exist, you can not upload it"); + } + + $element = $this->findElement($xpath, 1); + $tagName = $this->getTagName($xpath); + if ($tagName != "input") { + throw new DriverException("The element is not an input element, you can not attach a file to it"); + } + + $attributes = $this->getBrowser()->attributes($element["page_id"], $element["ids"][0]); + if (!isset($attributes["type"]) || $attributes["type"] != "file") { + throw new DriverException("The element is not an input file type element, you can not attach a file to it"); + } + + $this->browser->selectFile($element["page_id"], $element["ids"][0], $realPath); + } + + /** + * Puts the browser control inside the IFRAME + * You own the control, make sure to go back to the parent calling this method with null + * @param string $name + */ + public function switchToIFrame($name = null) { + //TODO: check response of the calls + if ($name === null) { + $this->browser->popFrame(); + return; + } else { + $this->browser->pushFrame($name); + } + } + + /** + * Focus on an element + * @param string $xpath + * @throws DriverException + */ + public function focus($xpath) { + $element = $this->findElement($xpath, 1); + $this->browser->trigger($element["page_id"], $element["ids"][0], "focus"); + } + + /** + * Blur on element + * @param string $xpath + * @throws DriverException + */ + public function blur($xpath) { + $element = $this->findElement($xpath, 1); + $this->browser->trigger($element["page_id"], $element["ids"][0], "blur"); + } + + /** + * Finds elements with specified XPath query. + * @param string $xpath + * @return NodeElement[] + * @throws DriverException When the operation cannot be done + */ + public function find($xpath) { + $elements = $this->browser->find("xpath", $xpath); + $nodeElements = array(); + + if (!isset($elements["ids"])) { + return null; + } + + foreach ($elements["ids"] as $i => $elementId) { + $nodeElements[] = new NodeElement(sprintf('(%s)[%d]', $xpath, $i + 1), $this->session); + } + return $nodeElements; + } + +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/check_element.js.twig b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/check_element.js.twig new file mode 100644 index 000000000..f3372ba7f --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/check_element.js.twig @@ -0,0 +1,35 @@ +{% autoescape 'js' %} +(function (xpath, check) { + function getPolterNode(xpath) { + var polterAgent = window.__poltergeist; + var ids = polterAgent.find("xpath", xpath, document); + return polterAgent.get(ids[0]); + } + + var pNode = getPolterNode(xpath); + + if (check && pNode.element.checked) { + //requested to check the element and is already check, do nothing. + return true; + } + + if (!check && pNode.element.checked == false) { + //move along nothing to be done + return true; + } + + if (check && pNode.element.checked == false) { + //we have to check the element, we will do so by triggering a click event so all change listeners are aware. + pNode.trigger("click"); + pNode.element.checked = true; + } + + if (!check && pNode.element.checked) { + //move along nothing to be done + pNode.trigger("click"); + pNode.element.checked = false; + return true; + } + return false; +}('{{xpath}}', {{check}})); +{% endautoescape %} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/execute_script.js.twig b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/execute_script.js.twig new file mode 100644 index 000000000..791276c51 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/execute_script.js.twig @@ -0,0 +1,3 @@ +{% autoescape false %} + {{ script }}; +{% endautoescape %} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/get_value.js.twig b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/get_value.js.twig new file mode 100644 index 000000000..8e040f854 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/get_value.js.twig @@ -0,0 +1,63 @@ +{% autoescape 'js' %} +(function (xpath) { + function getElement(xpath) { + var polterAgent = window.__poltergeist; + var ids = polterAgent.find("xpath", xpath, document); + var polterNode = polterAgent.get(ids[0]); + return polterNode.element; + } + + function inputRadioGetValue(element){ + var value = null; + var name = element.getAttribute('name'); + if (!name){ + return null; + } + var fields = window.document.getElementsByName(name); + var i; + var l = fields.length; + for (i = 0; i < l; i++) { + var field = fields.item(i); + if (field.form === element.form && field.checked) { + return field.value; + } + } + return null; + } + + var node = getElement(xpath); + var tagName = node.tagName.toLowerCase(); + var value = null; + if (tagName == "input") { + var type = node.type.toLowerCase(); + if (type == "checkbox") { + value = node.checked ? node.value : null; + } else if (type == "radio") { + value = inputRadioGetValue(node); + } else { + value = node.value; + } + } else if (tagName == "textarea") { + value = node.value; + } else if (tagName == "select") { + if (node.multiple) { + value = []; + for (var i = 0; i < node.options.length; i++) { + if (node.options[i].selected) { + value.push(node.options[i].value); + } + } + } else { + var idx = node.selectedIndex; + if (idx >= 0) { + value = node.options.item(idx).value; + } else { + value = null; + } + } + } else { + value = node.value; + } + return value; +}('{{ xpath }}')); +{% endautoescape %} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/is_checked.js.twig b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/is_checked.js.twig new file mode 100644 index 000000000..6b14cad6a --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/is_checked.js.twig @@ -0,0 +1,31 @@ +{% autoescape 'js' %} +(function (xpath) { + function getElement(xpath, within) { + var result; + if (within === null || within === undefined) { + within = document; + } + result = document.evaluate(xpath, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + if (result.snapshotLength !== 1) { + return null; + } + return result.snapshotItem(0); + } + + var node = getElement(xpath); + + if (node === null) { + return null; + } + + if(node.tagName.toLowerCase() != "input"){ + return null; + } + + if(node.type.toLowerCase() != "checkbox" && node.type.toLowerCase() != "radio"){ + return null; + } + + return node.checked; +}('{{ xpath }}')); +{% endautoescape %} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/is_selected.js.twig b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/is_selected.js.twig new file mode 100644 index 000000000..a3a18d307 --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/is_selected.js.twig @@ -0,0 +1,16 @@ +{% autoescape 'js' %} +(function (xpath) { + function getElement(xpath) { + var polterAgent = window.__poltergeist; + var ids = polterAgent.find("xpath", xpath, document); + var polterNode = polterAgent.get(ids[0]); + return polterNode.element; + } + + var node = getElement(xpath); + if(typeof node.selected == "undefined"){ + return null; + } + return node.selected; +}('{{xpath}}')); +{% endautoescape %} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/set_value.js.twig b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/set_value.js.twig new file mode 100644 index 000000000..605aec37a --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/Resources/Script/set_value.js.twig @@ -0,0 +1,213 @@ +{% autoescape 'js' %} +(function (xpath, value) { + function getElement(xpath, within) { + var result; + if (within === null || within === undefined) { + within = document; + } + result = document.evaluate(xpath, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + if (result.snapshotLength !== 1) { + return null; + } + return result.snapshotItem(0); + } + + function isInput(element) { + if (element === null || element === undefined) { + return false; + } + return (element.tagName.toLowerCase() == "input"); + } + + function isTextArea(element) { + if (element === null || element === undefined) { + return false; + } + return (element.tagName.toLowerCase() == "textarea"); + } + + function isSelect(element) { + if (element === null || element === undefined) { + return false; + } + return (element.tagName.toLowerCase() == "select"); + } + + function deselectAllOptions(element) { + var i, l = element.options.length; + for (i = 0; i < l; i++) { + element.options[i].selected = false; + } + } + + function xpathStringLiteral(s) { + if (s.indexOf('"') === -1) + return '"' + s + '"'; + if (s.indexOf("'") === -1) + return "'" + s + "'"; + return 'concat("' + s.replace(/"/g, '",\'"\',"') + '")'; + } + + function clickOnElement(element) { + // create a mouse click event + var event = document.createEvent('MouseEvents'); + event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + + // send click to element + element.dispatchEvent(event); + + //After dispatching the event let's wait for 2 seconds at least... + return setTimeout(function () { + }, 2); + } + + function dispatchChange(element) { + var tagName =element.tagName.toLowerCase(); + var elementType = element.getAttribute("type"); + if (tagName != "option" || (tagName == "input" && elementType == "radio")){ + return true; + } + //Force the change when element is option + var event; + event = document.createEvent('HTMLEvents'); + event.initEvent('change', true, false); + element.dispatchEvent(event); + return true; + } + + function selectOptionOnElement(element, option, multiple) { + var polterAgent = window.__poltergeist; + var escapedOption = xpathStringLiteral(option); + // The value of an option is the normalized version of its text when it has no value attribute + var optionQuery = ".//option[@value = " + escapedOption + " or (not(@value) and normalize-space(.) = " + escapedOption + ")]"; + var ids = polterAgent.find("xpath", optionQuery, element); + var polterNode = polterAgent.get(ids[0]); + var optionElement = polterNode.element; + + if (multiple || !element.multiple) { + if (!optionElement.selected) { + clickOnElement(optionElement); + optionElement.selected = true; + } + return dispatchChange(optionElement); + } + + deselectAllOptions(element); + clickOnElement(optionElement); + optionElement.selected = true; + return dispatchChange(optionElement); + } + + function selectSetValue(element, value) { + var option; + if ((Array.isArray && Array.isArray(value)) || (value instanceof Array)) { + deselectAllOptions(element); + for (option in value) { + if (value.hasOwnProperty(option)) { + selectOptionOnElement(element, value[option], true); + } + } + return true; + } + + selectOptionOnElement(element, value, false); + return true; + } + + function selectRadioValue(element, value) { + if (element.value === value) { + clickOnElement(element); + element.checked=true; + dispatchChange(element); + return true; + } + + var formElements = element.form.elements; + var name = element.getAttribute("name"); + var radioElement, i; + + if (!name) { + return null; + } + + for (i = 0; i < formElements.length; i++) { + radioElement = formElements[i]; + if (radioElement.tagName.toLowerCase() == 'input' && radioElement.type.toLowerCase() == 'radio' && radioElement.name === name) { + if (value === radioElement.value) { + clickOnElement(radioElement); + radioElement.checked=true; + dispatchChange(radioElement); + return true; + } + } + } + + return null; + } + + function inputSetValue(element, value, elementXpath) { + var allowedTypes = ['submit', 'image', 'button', 'reset']; + var elementType = element.type.toLowerCase(); + var textLikeInputType = ['file', 'text', 'password', 'url', 'email', 'search', 'number', 'tel', 'range', 'date', 'month', 'week', 'time', 'datetime', 'color', 'datetime-local']; + + if (allowedTypes.indexOf(elementType) !== -1) { + return null; + } + + if (elementType == "checkbox") { + var booleanValue = false; + if (value == "1" || value == 1) { + booleanValue = true; + } else if (value == "0" || value == 0) { + booleanValue = false; + } + if ((element.checked && !booleanValue) || (!element.checked && booleanValue)) { + clickOnElement(element); + dispatchChange(element); + } + return true; + } + + if (elementType == "radio") { + return selectRadioValue(element, value); + } + + if (textLikeInputType.indexOf(elementType) !== -1) { + return textAreaSetValue(elementXpath, value); + } + + //No support for the moment for file stuff or other input types + return null; + + } + + function textAreaSetValue(elementXpath, value) { + var polterAgent = window.__poltergeist; + var ids = polterAgent.find("xpath", elementXpath, document); + var polterNode = polterAgent.get(ids[0]); + polterNode.set(value); + return true; + } + + var node = getElement(xpath); + if (node === null) { + return null; + } + + if (isSelect(node)) { + return selectSetValue(node, value); + } + + if (isInput(node)) { + return inputSetValue(node, value, xpath); + } + + if (isTextArea(node)) { + return textAreaSetValue(xpath, value); + } + + //for the moment everything else also to textArea stuff + return textAreaSetValue(xpath, value); + +}('{{xpath}}', JSON.parse('{{ value }}'))); +{% endautoescape %} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/SessionTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/SessionTrait.php new file mode 100644 index 000000000..6443dffac --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/SessionTrait.php @@ -0,0 +1,50 @@ +started = true; + } + + /** + * Tells if the session is started or not + * @return bool + */ + public function isStarted() { + return $this->started; + } + + /** + * Stops the session completely, clean slate for the browser + * @return bool + */ + public function stop() { + //Since we are using a remote browser "API", stopping is just like resetting, say good bye to cookies + //TODO: In the future we may want to control a start / stop of the remove browser + return $this->reset(); + } + + /** + * Clears the cookies in the browser, all of them + * @return bool + */ + public function reset() { + $this->getBrowser()->clearCookies(); + $this->getBrowser()->reset(); + $this->started = false; + return true; + } +} diff --git a/vendor/jcalderonzumba/mink-phantomjs-driver/src/WindowTrait.php b/vendor/jcalderonzumba/mink-phantomjs-driver/src/WindowTrait.php new file mode 100644 index 000000000..92fc6ee3a --- /dev/null +++ b/vendor/jcalderonzumba/mink-phantomjs-driver/src/WindowTrait.php @@ -0,0 +1,64 @@ +browser->windowName(); + } + + /** + * Return all the window handles currently present in phantomjs + * @return array + */ + public function getWindowNames() { + return $this->browser->windowHandles(); + } + + /** + * Switches to window by name if possible + * @param $name + * @throws DriverException + */ + public function switchToWindow($name = null) { + $handles = $this->browser->windowHandles(); + if ($name === null) { + //null means back to the main window + return $this->browser->switchToWindow($handles[0]); + } + + $windowHandle = $this->browser->windowHandle($name); + if (!empty($windowHandle)) { + $this->browser->switchToWindow($windowHandle); + } else { + throw new DriverException("Could not find window handle by a given window name: $name"); + } + + } + + /** + * Resizing a window with specified size + * @param int $width + * @param int $height + * @param string $name + * @throws DriverException + */ + public function resizeWindow($width, $height, $name = null) { + if ($name !== null) { + //TODO: add this on the phantomjs stuff + throw new DriverException("Resizing other window than the main one is not supported yet"); + } + $this->browser->resize($width, $height); + } + +}