zenstruck / browser Goto Github PK
View Code? Open in Web Editor NEWA fluent interface for your Symfony functional tests.
License: MIT License
A fluent interface for your Symfony functional tests.
License: MIT License
Alternative to #39.
$this->browser()
// ...
->use(function(CookieJar $jar) {
$jar->expire('MOCKSESSID');
})
;
The feature to dump the source code for a failed test to a file is very useful. It's something Codeception has done for a long time, and as a previous Codeception user, the first thing I tried to do with the dump file from a failed test was to try to open it in a browser to investigate the failure (using the PHPStorm "open in browser" feature).
However, this does not work as expected as PHPStorm will not offer the installed browsers as options, because the file extension is .txt
. I see why this has been chosen, as the HTTP headers are also included at the start of the file before the source code.
Can I suggest that the source code file is instead saved with a .html
extension, and that the HTTP headers at the start of the file are wrapped in an HTML comment tag, to make it a valid HTML file. This way the extension will better represent what the file contents are, and it will allow direct opening in a browser, using either PHPStorm or the OS default application.
I will submit a PR with these changes, but wanted to first double check that this change is acceptable.
Thanks
I'm not sure if this is doable, as it seems like interceptRedirect()
is a KernelBrowser feature, while assertRedirectTo()
is a shared feature. However, I do think it's nice for DX if an error is thrown when asserting a redirection URL if it's followed (or somehow allow this assertion to work when following redirections).
Using version ^0.9.1 for zenstruck/browser
./composer.json has been updated
Running composer update zenstruck/browser --with-all-dependencies
Loading composer repositories with package information
Restricting packages listed in "symfony/symfony" to "6.0.*"
Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- behat/mink[v1.8.0, ..., v1.9.0] require symfony/css-selector ^2.7|^3.0|^4.0|^5.0 -> found symfony/css-selector[v2.7.0, ..., v2.8.52, v3.0.0, ..., v3.4.47, v4.0.0, ..., v4.4.27, v5.0.0, ..., v5.4.0] but it conflicts with your root composer.json require (^6.0).
- zenstruck/browser v0.9.1 requires behat/mink ^1.8 -> satisfiable by behat/mink[v1.8.0, v1.8.1, v1.9.0].
- Root composer.json requires zenstruck/browser ^0.9.1 -> satisfiable by zenstruck/browser[v0.9.1].
You can also try re-running composer require with an explicit version constraint, e.g. "composer require zenstruck/browser:*" to figure out if any version is installable, or "composer require zenstruck/browser:^2.1" if you know which you need.
Installation failed, reverting ./composer.json and ./composer.lock to their original content.
I think the culprit is that we don't allow for newer versions of behat/mink
. If there are no bc breaking changes, I can probably create a PR with this small change to see if this fixes the composer conflicts.
$this->browser()
// ...
->use(function(RequestDataCollector $collector) {
// ...
})
;
I have a ChoiceType in Symfony.
->add('assignedRoles', ChoiceType::class, [
'expanded' => true,
'multiple' => true,
'choices' => $this->allRoles,
'choice_label' => function ($item) { return 'system.role.'.str_replace("role_","", strtolower($item)); },
'choice_value' => function ($item) { return $item; }
])
HTML (github has problems with < and >)
<input type="checkbox" id="user_edit_form_assignedRoles_3" name="user_edit_form[assignedRoles][]" class="form-check-input" value="bar" />
<label class="form-check-label" for="user_edit_form_assignedRoles_3">bar</label>
<input type="checkbox" id="user_edit_form_assignedRoles_4" name="user_edit_form[assignedRoles][]" class="form-check-input" value="bar" />
<label class="form-check-label" for="user_edit_form_assignedRoles_4">foo</label>
Check/Uncheck of Checkboxes is working
->checkField('foo')
->uncheckField('foo')
But ->assertChecked('bar') is always true. But it is not checked.
->assertNotChecked('bar') fails too.
Am i doing something wrong?
I tried the assertNotSelected() function as well.
->assertNotSelected('user_edit_form[assignedRoles][]', 'foo')
Hi @kbond,
thank you for this great library, it's a real timesaver.
I'm not sure if this is the expected behavior but I stumbled across the following:
$this
->assertFieldEquals('flow_mailMessageCompose_step', '1') // doesn't work, 'Form field not found'
->assertElementAttributeContains('input[name="flow_mailMessageCompose_step"]', 'value', '1') // works
;
Thanks again.
Cheers
BjΓΆrn
PHPUnit's asserts (and all asserts provided by Symfony) allow you to specify a custom failure message as last argument. This gives you the possibility to add some more context to an otherwise vague error.
Would it be an idea to also add this feature to Browser?
Currently, uploading files is a bit verbose:
$browser->post('/endpoint', ['files' => [new UploadedFile($realpath, $name, test: true)]);
It would be better if you could do:
$browser->post('/endpoint', ['files' => [$realpath]);
And HttpOptions
auto-converts file paths's to UploadedFile
's.
Seems like the patch shortcut function is missing (like you can do $this->browser()->post|put|get|delete()
but not patch)
Adding that would be neat :)
This library already allows you to make json assertions using JMESPath but maybe we can do better?
https://twitter.com/enunomaduro/status/1369773695019933696
https://laravel.com/docs/8.x/http-tests#fluent-json-testing
Currently, if the file doesn't exist, it silently doesn't add.
Per conversation with @nikophil. Use justinrainbow/json-schema
to validate schema.
I don't have any experience with json schema but is it common the include the schema files in some kind of resource directory in your project? If so, maybe a BROWSER_JSON_SCHEMA_DIR
env variable could be used to set this dir, then with: $json->matchesSchema('post.json')
, post.json
would be loaded relative to this env variable?
Is it possible to configure the PantherBrowser that it introspects/pause the browser by an error?
Example:
->click('#form_wrong_id')
The Browser closes and an error will be shown.
Clickable element "#form_wrong_id" not found.
I insert a pause() command before the line and fix the selector. Delete the pause() command.
Check if it works and repeat the steps until it works.
Set an optional option (default: false)
$this->pantherBrowser(['SET_OPTION_PAUSE_BY_ERROR'] => true)
If an error will rise, it starts the introspection-Mode and give some Feedback about the error.
So i do not need add/remove the pause() function all over.
Hey,
I'm having issues with the HttpPlug Mock client:
https://docs.php-http.org/en/latest/integrations/symfony-bundle.html#usage-for-reusable-bundles
I can't seem to set request matches, the conditionalResults
in the Mock client is always empty.
I tried setting it using:
self::getContainer()
and
$this->browser()->getContainer()
but both seem to be the wrong ones.
Anyone know the right container the request is made against?
This base class has the base browser methods with KernelBrowser
/HttpBrowser
/PantherBrowser
extending and adding their own features. While there are no abstract methods currently, it would make it clear it is not to be used.
Is there any way that I can get the response?
Example:
$json = $this->browser()
->get('/api/foo')
->assertStatus(200)
->myJsonOutput();
$this->browser()
->get('/api/'.$json['my-key'])
->assertStatus(200);
But also, I may feel more comfortable running assertions on my body like I normally would... Or if my body is XML..
Greetings !
first of all, I really enjoy your library with its Developper eXperience friendly usage.
But we came to a cases with multiple file upload in a Symfony application.
Is there a way to test a multiple upload on a file input with multiple="multiple"
attribute ?
this kind of "generic code" ($files being an array of paths to fixtures files) :
$browser = $this
->browser()
->get('/form-with-file-input-path')
;
...
foreach($files as $file) {
$brower->attachFile('file_input[]', "$file");
}
$browser->click('submit_button');
doesn't seem to work : in the controller, the concerned file field, or the $request->files
seem to be systematicaly empty ....
thank you !
The idea is to click a form submit and ensure there are no validation errors. If there are, fail.
public function clickAndValidate(string $locator): self
{
return $this->clickAndIntercept($locator)
->use(function (ValidatorDataCollector $collector) {
Assert::that($collector->getViolationsCount())
->isEmpty('There were {actual} validation violations.')
;
})
;
}
Displaying the actual violations (not just the count) would be nice but the ValidatorDataCollector
makes this difficult as the violations are inside a Symfony\Component\VarDumper\Cloner\Data
object. One possibility is to actually dump
this but there is a lot of other data in here that isn't terribly useful and would just clutter your console.
I recently added the ability to pass options to the test browser factory methods (79e8130):
protected function pantherBrowser(array $options = [], array $kernelOptions = [], array $managerOptions = []): PantherBrowser
protected function httpBrowser(array $kernelOptions = [], array $pantherOptions = []): HttpBrowser
protected function kernelBrowser(array $options = []): KernelBrowser
I find all these different options confusing - what about just a single $options array for each:
protected function pantherBrowser(array $options = []): PantherBrowser
protected function httpBrowser(array $options = []): HttpBrowser
protected function kernelBrowser(array $options = []): KernelBrowser
The single $options
could be passed to each place it's needed. I don't think there would be any issues with key conflicts or extra keys.
In 5.1, Symfony's KernelBrowser
added the loginUser()
method. This should be added to Browser's KernelBrowser as actingAs()
.
$browser
->assertSeeIn('title', 'Post Title')
->assertSeeIn('h1', 'Post Title')
;
Could become:
$browser
->assertSeeIn(['title', 'h1'], 'Post Title')
;
In Symfony 5.1, we added a way to login in tests: https://symfony.com/blog/new-in-symfony-5-1-simpler-login-in-tests
It would be great if we can port the same functionality to browser.
$user = UserFactory::new()->create(['username' => 'john_user', 'password' => 'kitten']);
$this->kernelBrowser()
->loginAs($user)
->visit('/profile')
// ...
;
Currently, the following "dd" methods exist:
Browser::dd()
PantherBrowser::ddConsoleLog()
PantherBrowser::ddScreenshot()
Should 1 and 2 be consolidated into dd()
? If using the PantherBrowser
, anytime you call ->dd()
, the normal dd()
output is shown, the console log is dumped and a screenshot saved?
Facebook\WebDriver\Exception\JavascriptErrorException: javascript error: Failed to execute 'elementsFromPoint' on 'Document': The provided double value is non-finite.
(Session info: headless chrome=87.0.4280.88)
Related to #4
Hi,
I am migrating a large code base and try to use zenstruck/browser to replace the symfony client.
I am having some trouble as I am testing my login process. It's a json api, I have no problem with my admin authentication but when I am trying to login a normal user, the test fails.
I am surely not asking you to fix my test for me (but if you can without the code, you're very good π )
Here is my test, I reduced the code to the minimal required here (the only thing which is not visible is that I am using a dataProvider to get $username
and $password
):
$response = $this->browser()
->post('/api/login_check', [
'body' => [
'username' => $username,
'password' => $password,
],
])
->use(function (\Zenstruck\Browser\Response $response) {
self::assertSame(Response::HTTP_OK, $response->statusCode());
})
->response();
So the test fails at assertSame
as the $response->statusCode()
is 401. The problem remains the same if I change the ->use
callback to a classic assertSame
after the $response
assignation.
The problem is that my output looks like this:
PHPUnit 9.5.0 by Sebastian Bergmann and contributors.
Testing Functional\Tests\App\Infrastructure\Symfony\Controller\LoginCheckControllerTest
Undefined array key 1
THE ERROR HANDLER HAS CHANGED!
Remaining self deprecation notices (10)
Remaining indirect deprecation notices (29)
I have no message, no file created in the var/browser/
directory so I know my test fails because I added some dd
above the assertSame
but I think this is not the expected behavior π
Also var/log/test.log
has no information.
As you might imagine, it's a legacy code migration... The architecture is a bit complex but still very clean and from what I can say the legacy tests don't interfere with this new ones (as I use different test suites and only runs the suite I am interested in, plus the test above only executes one test). I might have missed something and hopefully someone could point it out? Or at least give me some advice as I don't know where to look at for now.
If the test fails outside of the browser context (my fixtures are not loaded properly for example) I get a regular phpunit error stack trace.
Thank you for your help,
Please ask if I could add more information I didn't think of.
Hello,
I have troubles mocking some HttpClient
instance with the browser after validating a form.
Here is the code:
$this->browser()
->use(function () {
self::getContainer()->set('test.http_client', new MockHttpClient(new MockResponse()));
})
->visit('/') // some http calls are made here, using the mock client
->assertSuccessful()
->fillField('form[date]', '2021-11-10') // fill some fields
->click('form_search') // this click redirects us on the same page
->assertSuccessful() // :boom: 500 => the http_client is not mocked
Adding some dumps give me some clues
->use(function () {
// prints Symfony\Component\HttpClient\MockHttpClient
dump(self::getContainer()->get('test.http_client')::class);
})
->click('form_search')
->use(function () {
// prints Symfony\Component\HttpClient\RetryableHttpClient
dump(self::getContainer()->get('test.http_client')::class);
})
any ideas?
I saw this issue which is somehow related, but I don't even know where I could reuse the same "mocked container" or something else...
thanks for your help!
Dump validation errors: laravel/framework#38046
$this->pantherBrowser()
->visit('/page')
->click('.something') // element not in view part
;
// Facebook\WebDriver\Exception\MoveTargetOutOfBoundsException: move target out of bounds
Possible solution:
$this->pantherBrowser()
->visit('/page')
->use(function (Browser $browser ){
$x = $browser->client()->findElement(WebDriverBy::cssSelector('.something'))->getLocation()->getX();
$y = $browser->client()->findElement(WebDriverBy::cssSelector('.something'))->getLocation()->getY();
$string = 'window.scrollTo({top:'.$y.', left:'.$x.', behaviour: \'auto\'});';
$browser->client()->executeScript($string);
})
->waitUntilVisible('.something')
->click('.something') // element not in view part
;
Say I have a "customer lookup" field which uses an AJAX request to search for possible matches via a backend endpoint. On page load the <select>
contains no options.
How can I populate this field with a value? I know what the ID of the value should be, but obviously the Javascript lookup to the backend does not work.
I have tried ->selectField('Customer', '1234')
and also ->fillField('Customer', '1234')
but neither are working.
SelectField() gives me an error such as:
Select option with value|text "1234" not found.
FillField() gives me an error such as:
...cannot take "1234" as a value (possible values: "")
Is there a way to just define the name of a form field and the value that should be sent when submitting the form?
Any pointers appreciated. Thanks.
Hi Kevin,
Is there any support, or plans, to support testing CSV responses? I guess in a similar manner to the JSON response testing that's currently available.
Thanks
The 30 second default is pretty long, perhaps browser's default should be lower?
This function, when not running your tests in headless mode, pauses the test and allows you to view what's going on in the real browser. Ryan pointed out the name isn't great, some alternatives:
open()
pause()
(might get mistaken for wait()
)freeze()
Html
into Browser
Json
into BrowserKitBrowser
Http
into BrowserKitBrowser
HttpOptions
to Zenstruck/Browser
namespace (or Zenstruck/Browser/Util
?)Mailer
trait/component into BrowserKitBrowser
?ProfileAware
interface, Mailer
component and traitTestEmail
to Zenstruck/Browser
namespace (or Zenstruck/Browser/Util
?)Greetings !
first of all, I really enjoy your library with its Developper eXperience friendly usage.
But we came to cases with multiple file upload in a Symfony application.
Is there a way to test a multiple upload on a file input with multiple="multiple"
attribute ?
this kind of "generic code" ($files
being an array of paths to fixtures files) :
$browser = $this
->browser()
->get('/form-with-file-input-path')
;
...
foreach($files as $file) {
$brower->attachFile('file_input[]', $file);
}
$browser->click('submit_button');
doesn't seem to work : in the controller, the concerned file field, or the $request->files
seem to be systematicaly empty ....
thank you !
EDIT: sorry, in the controller, the concerned file field actually contains the last file path from the $files
array. But it's problematic on forms with constraints like for example "at least 3 files are required"
I just found something that was a bit weird.
$this->kernelBrowser()
->actingAs(AuthenticatedUserFactory::fromCandidate($candidate), 'default')
->visit(\sprintf('/candidate/applications/%s/name', $application->getUuid()))
->assertStatus(200)
->assertSeeElement('#add_name_firstName')
->fillField('add_name_firstName', 'Test')
->fillField('add_name_lastName', 'Testsson')
->fillField('add_name_phoneNumber', '0123456789')
->click('add_name_submit')
->assertRedirectedTo(\sprintf('/candidate/applications/%s/background', $application->getUuid()));
This will not work because when I submit the form, there is a 302 and then a page that answer 200. Since Im not using interceptRedirects()
, Browser will never see the 302 and the test will fail.
A simple workaround for me is just to add ->interceptRedirects()
. Im not sure any changes are needed, I just wanted to share my experience.
Hi,
I am still using this lib to recreate my functional tests. I used to use self::assertSame($expectedErrors, $apiErrors)
to test if my form errors are the one I expect (I test on api endpoint, not on the symfony type)
When migrating I use the assertJsonMatches('errors', $expectedErrors)
method on the browser.
I have a difference between the two assertion methods. The first one (based on phpunit) did not take order in account. The second method (based on zendstruck/assert lib) did it. Which is a bit annoying as I don't care of order of my errors, I only need to know if they are all present.
Is it something I am the only one with this need? Maybe we could change this behavior to have the same as the phpunit's assertSame
method?
What do you think?
I have read the README, it says
` public function test_using_kernel_browser(): void
{
$this->browser()
->visit('/my/page')
->assertSuccessful()
;
}
/**
* Requires this test extend Symfony\Component\Panther\PantherTestCase.
*/
public function test_using_panther_browser(): void
{
$this->pantherBrowser()
->visit('/my/page')
->assertSuccessful()
;
}`
but if i test with pantherbrowser i have this error:
1) App\Tests\HomeControllerTest::testSomething Error: Call to undefined method Zenstruck\Browser\PantherBrowser::assertSuccessful()
I have read src/Browser/PantherBrowser.php and Browser.php
and i has not AssertSuccessfull.
Ability to catch and assert an exception was thrown for the next request:
$browser
->expectException(SomeException::class) // auto-enables ->throwExceptions() for the next request
->post('/some/url') // fail if SomeException wasn't thrown
;
Make assertions on the exception:
$browser
->expectException(function(SomeException $e) {
$this->assertStringContainsString('expected message', $e->getMessage());
// any other assertions (ie ensure database entry wasn't modified)
})
->post('/some/url') // fail if SomeException wasn't thrown
;
$browser
->clickAndIntercept('button')
// make assertions on the redirect with profiler enabled
// same as:
->interceptRedirects()
->withProfiling()
->click('button')
;
$browser
->clickAndValidate('button') // intercept redirect with profiler, fail if (form) validation errors
;
I was slightly confused by the name "parameters", which actually represent query options.
A look at Symfony itself shows BrowserKit's Request
uses "parameters", but HttpFoundation's Request
uses "query" (e.g. $request->query->get('q')
). As such, I think the average Symfony developer is more familiar with "query".
As another reference, Guzzle also uses the "query" option name (https://docs.guzzlephp.org/en/stable/request-options.html#query).
Hello !
In this context, in functional tests :
as noticed here, the multiple button check with $form->getClickedButton()
doesn't seem to work when following the recommandation in best practices and put it in controller or in form - this debug check in controller :
if ($form->isSubmitted() && $form->isValid()) {
dd($form->getClickedButton());
}
systematically outputs null
.
Even the solution brought here and "slightly"(as illustration) followed like this in our case :
<input type="submit" name="submit" value="draft"/>
<button type="submit" name="submit" value="publish">
publish
</button>
the following test :
$this->browser()
->get('/form-path')
... // filling object fields
->click('draft')
;
also actually outputs null
when debugging it in controller like this :
if ($form->isSubmitted() && $form->isValid()) {
dd($request->request->get('submit'));
}
dd($request->request);
somehow only outputs all other submited data except the 'submit' button.
It seems there's somewhere a process for SF5 where the form is cleaned up from submit buttons or there's something I'm missing...
Out of the test context, I manage to have the cliked button in both cases (in template using $request->request->get('submit')
or in controller/form using $form->getClickedButton()
) ...
It is considered best practice to hard-code the URL paths into your tests, so that if a route is modified, the test fails. This ensures a consistent user experience, is good for SEO etc. However, this only really applies to public website type apps.
I build private, Saas type apps, where the URL changing is no big deal. I find it much easier to work with route names, which are defined as constants in the controller classes.
The browser has methods which require the path to test against - e.g.
visit(string $path)
assertOn(string $path)
I think it would be handy to have a matching set of methods, that support passing a route name and parameters instead of the path:
visitRoute(string $route, array $parameters = [])
assertOnRoute(string $route, array $parameters = [])
Is this something that could be incorporated into this package, or something better implemented by my app extending things?
If extending things is the way to go, where can I inject the router service?
Thanks
The method parameter $selector
is used in several methods and means different things depending on the context:
->assertSeeIn('h1', 'some text')
or ->dump('h1')
->follow('some link')
->click('button')
->fillField('name', 'Kevin')
->assertJsonMatches('foo.bar.baz', 1)
or ->dump('foo.bar.baz')
(when content-type is json)I think adding good docblocks would help but there are some things that can be improved:
Right now, methods that select elements with link/button/field selectors cannot use css selectors to find the element. I think, if unable to find the element via the link/button/field selector, assume a css selector and try.
This opens up the possibility of clicking buttons via tr:contains(First Post) a.btn
(think admin list of entities each with an edit button).
This problem is not related to this repo.
But i hope it will be useful for other users.
I found a problem after i started to test an E-Mail-Field at start useing the "@" -Char.
Facebook\WebDriver\Exception\UnknownErrorException : unknown error: Cannot construct KeyEvent from non-typeable key (Session info: chrome=98.0.4758.80)
Google found this Issue.
https://groups.google.com/g/chromedriver-users/c/8WTsbereIO4
My Test-Environment is Mac M1 with Chrome 98 and the driver installed via Homebrew. Non English Keyboard Layout
A temporary fix is to add english as keyboard language and switch to it.
Now tests will run fine.
$this->wrapMinkExpectation(
fn() => $this->webAssert()->elementExists('xpath', $xpathString)
);
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.