I'm using Goutte (which is a standalone package of the Symfony functional testing components) to test a Wordpress theme:
public function testDocumentHasBasicNodes(){
$this->assertEquals(1, $this->crawler->filter('title')->count(),
'document shall have a TITLE node');
$this->assertEquals(1, $this->crawler->filter('meta')->count(),
'document shall have a meta[charset="utf-8"] node');
}
The first test passes but:
indexTest::testDocumentHasBasicNodes
document shall have a meta[charset="utf-8"] node
Failed asserting that 0 matches expected 1.
So to sum up my question:
How do I get a node by attribute with Goutte / Symfony CSS Selector component?
You may filter by attribute like this.
$this->assertEquals(1, $this->crawler->filter('meta[charset="utf-8"]')->count(),
'document shall have a meta[charset="utf-8"] node');
Related
I am in charge of upgrading easyadmin bundle on an app that was previously built using symfony v4.4.19. Initially we had: easycorp/easyadmin-bundle v2.3.12. Then, we decided to upgrade the easyadmin bundle to v3 because we faced some issues when enabling/disabling a boolean property from the list view.
When I was using the v2 :
php bin/console debug:router showed a route called easyadmin with a path /myworkshop/ .
I had no Dashboard controller nor NecklaceCrudController, I simply had a controlller called
AccessoriesController.php with several actions like deleteAction that is executed when the user
deletes an entity, editAction when the user edits an entity, SearchAction ...
In the deleteAction there is this line of code:
return $this->redirect($this->generateUrl('easyadmin', array('action' => 'list', 'entity'=> $this->entity['name'])));
so the url would become something like this
/myworkshop/?action=list&entity=necklace
To open the easy admin interface I have to click on a menu link whose link is :
->createItem('Visit my workshop', ['route' => 'easyadmin']);
When I open this interface /references, I get the list of the different entities in my app, if
I select one, I see the list view and I can edit one entity successfully but I cannot
enable/disable boolean properties from the list view as mentioned earlier.
We specified /myworkshop instead of /admin in app>config>routing.yml
# easy admin
easy_admin_bundle:
resource: "#myShop/Controller/AccessoriesController.php"
type: annotation
prefix: /myworkshop```
- A custom css was successfully employed in : app>config>config.yml
easy_admin:
design:
assets:
css:
- 'bundles/css/easyadmin.css
When I upgraded to v3:
php bin/console debug:router showed a route called myshop_admin_dashboard_index (which was
automatically generated) with a path /easyadmin.
Dashboard controller and NecklaceCrudController were created, The DashboardController only has configureCrud() and configureMenuItems() functions. The latter contains the links yield MenuItem::linkToCrud . Question 1 : In version 2 no menu links where created explicitely like here, so I was wondering how was the complete list of entities correctly showing up on my application interface?
In DashboardController there is no index () function nor a route nor a link just configureCrud() and configureMenuItems() functions.
I want to keep the AccessoriesController.php with his several actions but now, with the new route and path, it is completely being ignored. Question 2 : Is there something that I have to change in the generateUrl part? can someone give me an example of what this will become if I opt for adminUrlgenerator like I read in the documentation?
The routing.yml file remains the same however, my easyadmin interface appears only when visiting this link /easyadmin instead of /myworkshop . Question 3: I want to keep the /workshop url , what should I do in addition to keeping the routing.yml as it is now?
I wish we could change myshop_admin_dashboard_index to easyadmin and /easyadmin to my /myworkshop as it was in version 2, because there are many parts in AccessoriesController where I use $this->generateUrl('easyadmin',
Question 4: The css is no longer applicable any idea why? could be related to the AccessoriesController that is not currently being taken into account.
Well, you can add the index method to your dashboard controller with route annotation to change the route
class DashboardController extends AbstractDashboardController
{
/**
* #Route("/myworkshop", name="admin")
*/
public function index(): Response
{
return $this->render('dashboard/index.html.twig');
}
You can add any route to easyadmin menu like this
class DashboardController extends AbstractDashboardController
{
public function configureMenuItems(): iterable
{
yield MenuItem::linktoRoute('Some Route', 'fa fa-info', 'route_name_here');
#...
}
}
You can add any CSS/js file too
class DashboardController extends AbstractDashboardController
{
public function configureAssets(): Assets
{
return Assets::new()
->addCssFile('build/admin.css')
->addJsFile('build/admin.js')
;
}
}
I seem to be facing the general problem that directive driven mutations won't properly work when unit testing them. Using GraphQL Playground everything works as expected and the User model is auto detected. But if unit testing the same functionality it fails.
All config values are properly set (as I can see from regular requests not failing):
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => ClientName\PackageName\Models\User::class,
],
...
]
And lighthouse config values also set to the namespace above!
So for example this mutation would trigger the UserPolicy class and after that automatically call the create method of the User class:
type Mutation #protect(guards: ["api"]) {
"Create user"
createUser(data: NewUserInput! #spread): User
#can(ability: "create")
#create
}
But calling the same request via phpunit would result in errors:
for #can it returns a not authorized error and if I remove #can it only updates the email field of the user model as if mass assignment is not properly set.
When I now change the above schema definition to:
type Mutation #protect(guards: ["api"]) {
"Create user"
createUser(data: NewUserInput! #spread): User
#can(ability: "create" model: "ClientName\\PackageName\\Models\\User")
#create(model: "ClientName\\PackageName\\Models\\User")
}
And it works as expected within phpunit tests!
Now I suspect that within tests there is not User model found or not the appropriate but I cannot understand why this is.
Does anyone have any idea?
in TYPO3 7.6 I defined a Frontend Plugin which is of type "PLUGIN_TYPE_CONTENT_ELEMENT".
ext_localconf.php:
ExtensionUtility::configurePlugin(
'MyVendor.' . $_EXTKEY,
'Maildownload',
['Maildownload' => 'show'],
[],
ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);
the configurePlugin method creates a typoScript Setup, where the the "run()" method of Extbase's Bootstrap class is called:
...
tt_content.myext_maildownload.20 = USER
tt_content.myext_maildownload20.userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
...
This means Extbase processes the request by calling the action "show" in the controller "Maildownload" of my extension.
I Call my plugin via ajax. That means, I define a page with a separate typenum and include the plugin via typoScript:
setup.ts:
ajax_page = PAGE
ajax_page {
typeNum = 123
config.disableAllHeaderCode = 1
config.metaCharset = UTF-8
config.debug = 0
config.additionalHeaders = Content-type:application/json
config.xhtml_cleaning = 0
config.adminPanel = 0
10 < tt_content.myext_maildownload
}
When I call the page index.php?type=123 the action "show" in the controller "Maildownload" will be executed.
After upgrading to TYPO3 8.7 this behaviour has obviously changed.
The generated typoScript code of the "configurePlugin()" method is still the same. When I call page index.php?type=123 TYPO3 renders it like an ordinary content element and does not call the TYPO3\CMS\Extbase\Core\Bootstrap->run method anymore. So my showAction method will not be called.
I could define a dataProcessor class now and move the showAction into it.
But that is not a great solution, because I don't want to bypass the complete extbase renderering process.
Is there now a new way to include my plugin into the page so that it will be treated like an extbase plugin and not like a content element?
#andrei thank you for your reply, but it did not hit the point.
Meanwhile I found out that the cause for the problem was this breaking change in TYPO3 8.5: https://docs.typo3.org/typo3cms/extensions/core/Changelog/8.5/Breaking-78002-EnforceCHashArgumentForExtbaseActions.html?highlight=chash
so a possible solution is to set the requireCHashArgumentForActionArguments to false in the typoScript plugin settings:
plugin.tx_myextension {
features.requireCHashArgumentForActionArguments = 0
...
}
I was learning about unit testing and I attempted to resolve the following issue:
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for zfcUserAuthentication
... using the only answer given at:
Simple ZF2 Unit Tests for a controller using ZfcUser
So my setUp function looks the same. Unfortunately, I get the error message:
Zend\Mvc\Exception\InvalidPluginException: Plugin of type Mock_ZfcUserAuthentication_868bf824 is invalid; must implement Zend\Mvc\Controller\Plugin\PluginInterface
It is caused at this part of the code (split up in my code in the same way):
$this -> controller->getPluginManager()
->setService('zfcUserAuthentication', $authMock); // Error refers to this line.
The $authMock object is apparently not implementing plugininterface, which I need to implement to pass into setService.
Is $authMock not meant to be passed there for it's use in unit testing? Should I be using a different (unit-testing oriented) setService method?
I need a way to handle logging into my application, or my unit testing is pointless.
Thanks for any advice.
=== Edit (11/02/2013) ===
I wanted to focus on this part for clarification, as I think this is the problem area:
// Getting mock of authentication object, which is used as a plugin.
$authMock = $this->getMock('ZfcUser\Controller\Plugin\ZfcUserAuthentication');
// Some expectations of the authentication service.
$authMock -> expects($this->any())
-> method('hasIdentity')
-> will($this->returnValue(true));
$authMock -> expects($this->any())
-> method('getIdentity')
-> will($this->returnValue($ZfcUserMock));
// At this point, PluginManager disallows mock being assigned as plugin because
// it will not implement plugin interface, as mentioned.
$this -> controller->getPluginManager()
->setService('zfcUserAuthentication', $authMock);
If the mock doesn't handle necessary implementations, how else am I to pretend to login?
You have a problem with name-spacing or your autoloader.
When you are creating your mock, the class definition of ZfcUser\Controller\Plugin\ZfcUserAuthentication is not being found. So PHPUnit creates a mock that only extends this class for your test. If the class was available then PHPUnit will use the actual class to extend when making its mock, which will then use the parent classes/interfaces.
You can see this logic here: https://github.com/sebastianbergmann/phpunit-mock-objects/blob/master/PHPUnit/Framework/MockObject/Generator.php
if (!class_exists($mockClassName['fullClassName'], $callAutoload) &&
!interface_exists($mockClassName['fullClassName'], $callAutoload)) {
$prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n";
if (!empty($mockClassName['namespaceName'])) {
$prologue = 'namespace ' . $mockClassName['namespaceName'] .
" {\n\n" . $prologue . "}\n\n" .
"namespace {\n\n";
$epilogue = "\n\n}";
}
$cloneTemplate = new Text_Template(
$templateDir . 'mocked_clone.tpl'
);
So if there is no class or interface, PHPUnit will actually create one itself so that the mock will meet the type hinting of original class name. However, any parent classes or interfaces will not be included because PHPUnit is not aware of them.
This would be due to not including the proper namespace in your test or having a problem in your autoloader. It is difficult to tell without actually seeing the entire test file.
Alternatively rather than mocking ZfcUser\Controller\Plugin\ZfcUserAuthentication, you could mock the Zend\Mvc\Controller\Plugin\PluginInterface in your test and pass that into the plugin manager. Though if you are type-hinting for the plugin in your code, your test still won't work.
//Mock the plugin interface for checking authorization
$authMock = $this->getMock('Zend\Mvc\Controller\Plugin\PluginInterface');
// Some expectations of the authentication service.
$authMock -> expects($this->any())
-> method('hasIdentity')
-> will($this->returnValue(true));
$authMock -> expects($this->any())
-> method('getIdentity')
-> will($this->returnValue($ZfcUserMock));
$this -> controller->getPluginManager()
->setService('zfcUserAuthentication', $authMock);
I just made an example for the FlashMessenger plugin. You should just use the ControllerPluginManager to override the ControllerPlugin. Make sure that your application bootstrap calls setApplicationConfig();
<?php
namespace SimpleTest\Controller;
use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
class SimpleControllerTest extends AbstractHttpControllerTestCase {
public function testControllerWillAddErrorMessageToFlashMessenger()
{
$flashMessengerMock = $this->getMockBuilder('\Zend\Mvc\Controller\Plugin\FlashMessenger', array('addErrorMessage'))->getMock();
$flashMessengerMock->expects($this->once())
->method('addErrorMessage')
->will($this->returnValue(array()));
$serviceManager = $this->getApplicationServiceLocator();
$serviceManager->setAllowOverride(true);
$serviceManager->get('ControllerPluginManager')->setService('flashMessenger', $flashMessengerMock);
$this->dispatch('/error/message');
}
}?>
I know that the basis of Silex approach in which all the application logic in a single file. But my application will be possible to have more than twenty controllers. So I want to have a handy map to manage the router.
My question is to search for solutions in which I would be able to make a router to a separate file. In the best case, the file must be of YAML type:
# config/routing.yml
_home:
pattern: /
defaults: { _controller: MyProject\Controller\MyController::index }
But the native is also a good case (for me):
$routes = new RouteCollection();
$routes->add(
'home',
new Route('/', array('controller' => 'MyProject\Controller\MyController::index')
));
return $routes;
Problem of the second case is that I have to use the match() function for each rule of routing. It is not at all clear.
What are the ways to solve this issue? The condition is that I want to use the existing API Silex or components of Symfony2.
Small note:
I don't use a ControllerProviderInterface for my Controller classes. This is an independent classes.
First of all, the basis of Silex is not that you put everything in one file. The basis of Silex is that you create your own 'framework', your own way of organizing applications.
"Use silex if you are comfortable with making all of your own architecture decisions and full stack Symfony2 if not."
-- Dustin Whittle
Read more about this in this blogpost, created by the creator of Silex.
How to solve your problem
What you basically want is to parse a Yaml file and get the pattern and defaults._controller settings from each route that is parsed.
To parse a Yaml file, you can use the Yaml Component of Symfony2. You get an array back which you can use to add the route to Silex:
// parse the yaml file
$routes = ...;
$app = new Silex\Application();
foreach ($routes as $route) {
$app->match($route['pattern'], $route['defaults']['_controller']);
}
// ...
$app->run();
I thought I'd add my method here as, although others may work, there isn't really a simple solution. Adding FileLocator / YamlFileLoader adds a load of bulk that I don't want in my application just to read / parse a yaml file.
Composer
First, you're going to need to include the relevant files. The symfony YAML component, and a really simple and useful config service provider by someone who actively works on Silex.
"require": {
"symfony/yaml": "~2.3",
"igorw/config-service-provider": "1.2.*"
}
File
Let's say that your routes file looks like this (routes.yml):
config.routes:
dashboard:
pattern: /
defaults: { _controller: 'IndexController::indexAction' }
method: GET
Registration
Individually register each yaml file. The first key in the file is the name it will be available under your $app variable (handled by the pimple service locator).
$this->register(new ConfigServiceProvider(__DIR__."/../config/services.yml"));
$this->register(new ConfigServiceProvider(__DIR__."/../config/routes.yml"));
// any more yaml files you like
Routes
You can get these routes using the following:
$routes = $app['config.routes']; // See the first key in the yaml file for this name
foreach ($routes as $name => $route)
{
$app->match($route['pattern'], $route['defaults']['_controller'])->bind($name)->method(isset($route['method'])?$route['method']:'GET');
}
->bind() allows you to 'name' your urls to be used within twig, for example.
->method() allows you to specify POST | GET. You'll note that I defaulted it to 'GET' with a ternary there if the route doesn't specify a method.
Ok, that's how I solved it.
This method is part of my application and called before run():
# /src/Application.php
...
protected function _initRoutes()
{
$locator = new FileLocator(__DIR__.'/config');
$loader = new YamlFileLoader($locator);
$this['routes'] = $loader->load('routes.yml');
}
Application class is my own and it extends Silex\Application.
Configuration file:
# /src/config/routes.yml
home:
pattern: /
defaults: { _controller: '\MyDemoSite\Controllers\DefaultController::indexAction' }
It works fine for me!
UPD:
I think this is the right option to add collections:
$this['routes']->addCollection($loader->load('routes.yml'));
More flexible.
You could extend the routes service (which is a RouteCollection), and load a YAML file with FileLocator and YamlFileLoader:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
$app->extend('routes', function($routeCollection) {
$locator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');
$routeCollection->addCollection($collection);
return $routeCollection;
});
You will need symfony/config and symfony/yaml dependencies though.