Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ $browser
->selectField('Type', 'Employee') // "select" single option
->selectField('Notification', ['Email', 'SMS']) // "select" multiple options
->attachFile('Photo', '/path/to/photo.jpg')
->attachFile('Photo', ['/path/to/photo1.jpg', '/path/to/photo2.jpg') // attach multiple files (if field supports this)
->click('Submit')

// ASSERTIONS
Expand Down
13 changes: 9 additions & 4 deletions src/Browser.php
Original file line number Diff line number Diff line change
Expand Up @@ -244,15 +244,20 @@ final public function selectFieldOptions(string $selector, array $values): self
}

/**
* @param string[]|string $filename string: single file
* array: multiple files
*
* @return static
*/
final public function attachFile(string $selector, string $path): self
final public function attachFile(string $selector, $filename): self
{
if (!\file_exists($path)) {
throw new \InvalidArgumentException(\sprintf('File "%s" does not exist.', $path));
foreach ((array) $filename as $file) {
if (!\file_exists($file)) {
throw new \InvalidArgumentException(\sprintf('File "%s" does not exist.', $file));
}
}

$this->documentElement()->attachFileToField($selector, $path);
$this->documentElement()->attachFileToField($selector, $filename);

return $this;
}
Expand Down
69 changes: 36 additions & 33 deletions src/Browser/Mink/BrowserKitDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
use Symfony\Component\DomCrawler\Field\FileFormField;
use Symfony\Component\DomCrawler\Field\FormField;
use Symfony\Component\DomCrawler\Field\InputFormField;
use Symfony\Component\DomCrawler\Field\TextareaFormField;
use Symfony\Component\DomCrawler\Form;
use Symfony\Component\HttpKernel\HttpKernelBrowser;
Expand Down Expand Up @@ -419,13 +418,35 @@ public function isChecked($xpath)

public function attachFile($xpath, $path)
{
$files = (array) $path;
$field = $this->getFormField($xpath);

if (!$field instanceof FileFormField) {
throw new DriverException(\sprintf('Impossible to attach a file on the element with XPath "%s" as it is not a file input', $xpath));
}

$field->upload($path);
$field->upload(\array_shift($files));

if (empty($files)) {
// not multiple files
return;
}

$node = $this->getFilteredCrawler($xpath);

if (null === $node->attr('multiple')) {
throw new \InvalidArgumentException('Cannot attach multiple files to a non-multiple file field.');
}

$fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
$form = $this->getFormForFieldNode($fieldNode);

foreach ($files as $file) {
$field = new FileFormField($fieldNode);
$field->upload($file);

$form->set($field);
}
}

public function submitForm($xpath)
Expand Down Expand Up @@ -489,18 +510,25 @@ protected function getFormField($xpath)
$fieldNode = $this->getCrawlerNode($this->getFilteredCrawler($xpath));
$fieldName = \str_replace('[]', '', $fieldNode->getAttribute('name'));

$form = $this->getFormForFieldNode($fieldNode);

if (\is_array($form[$fieldName])) {
return $form[$fieldName][$this->getFieldPosition($fieldNode)];
}

return $form[$fieldName];
}

private function getFormForFieldNode(\DOMElement $fieldNode): Form
{
$formNode = $this->getFormNode($fieldNode);
$formId = $this->getFormNodeId($formNode);

if (!isset($this->forms[$formId])) {
$this->forms[$formId] = new Form($formNode, $this->getCurrentUrl());
}

if (\is_array($this->forms[$formId][$fieldName])) {
return $this->forms[$formId][$fieldName][$this->getFieldPosition($fieldNode)];
}

return $this->forms[$formId][$fieldName];
return $this->forms[$formId];
}

/**
Expand Down Expand Up @@ -620,7 +648,7 @@ private function submit(Form $form)
$formId = $this->getFormNodeId($form->getFormNode());

if (isset($this->forms[$formId])) {
$this->mergeForms($form, $this->forms[$formId]);
$form = $this->forms[$formId];
}

// remove empty file fields from request
Expand Down Expand Up @@ -711,31 +739,6 @@ private function getOptionValue(\DOMElement $option)
return '1'; // DomCrawler uses 1 by default if there is no text in the option
}

/**
* Merges second form values into first one.
*
* @param Form $to merging target
* @param Form $from merging source
*/
private function mergeForms(Form $to, Form $from)
{
foreach ($from->all() as $name => $field) {
$fieldReflection = new \ReflectionObject($field);
$nodeReflection = $fieldReflection->getProperty('node');
$valueReflection = $fieldReflection->getProperty('value');

$nodeReflection->setAccessible(true);
$valueReflection->setAccessible(true);

$isIgnoredField = $field instanceof InputFormField &&
\in_array($nodeReflection->getValue($field)->getAttribute('type'), ['submit', 'button', 'image'], true);

if (!$isIgnoredField) {
$valueReflection->setValue($to[$name], $valueReflection->getValue($field));
}
}
}

/**
* Returns DOMElement from crawler instance.
*
Expand Down
8 changes: 7 additions & 1 deletion src/Browser/Mink/PantherDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,13 @@ public function selectOption($xpath, $value, $multiple = false): void

public function attachFile($xpath, $path): void
{
$this->fileFormField($xpath)->upload($path);
if (\is_array($path) && empty($this->filteredCrawler($xpath)->attr('multiple'))) {
throw new \InvalidArgumentException('Cannot attach multiple files to a non-multiple file field.');
}

foreach ((array) $path as $file) {
$this->fileFormField($xpath)->upload($file);
}
}

public function isChecked($xpath): bool
Expand Down
26 changes: 26 additions & 0 deletions tests/BrowserTests.php
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,32 @@ public function cannot_attach_file_that_does_not_exist(): void
;
}

/**
* @test
*/
public function can_attach_multiple_files(): void
{
$this->browser()
->visit('/page1')
->attachFile('Input 9', [__DIR__.'/Fixture/files/attachment.txt', __DIR__.'/Fixture/files/xml.xml'])
->click('Submit')
->assertContains('"input_9":["attachment.txt","xml.xml"]')
;
}

/**
* @test
*/
public function cannot_attach_multiple_files_to_a_non_multiple_input(): void
{
$this->expectException(\InvalidArgumentException::class);

$this->browser()
->visit('/page1')
->attachFile('Input 5', [__DIR__.'/Fixture/files/attachment.txt', __DIR__.'/Fixture/files/xml.xml'])
;
}

/**
* @test
*/
Expand Down
13 changes: 12 additions & 1 deletion tests/Fixture/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,20 @@ public function xml(): Response

public function submitForm(Request $request): JsonResponse
{
$files = \array_map(
static function($value) {
if (\is_array($value)) {
return \array_map(fn(UploadedFile $file) => $file->getClientOriginalName(), $value);
}

return $value instanceof UploadedFile ? $value->getClientOriginalName() : null;
},
$request->files->all()
);

return new JsonResponse(\array_merge(
$request->request->all(),
\array_map(fn(UploadedFile $file) => $file->getClientOriginalName(), \array_filter($request->files->all()))
\array_filter($files)
));
}

Expand Down
3 changes: 3 additions & 0 deletions tests/Fixture/files/page1.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ <h1>h1 title</h1>
<label for="radio3">Radio 3</label>
<input type="radio" id="radio3" name="input_8" value="option 3">

<label for="input9">Input 9</label>
<input id="input9" name="input_9[]" type="file" multiple>

<button type="submit" name="submit_1" value="a">Submit</button>
<button type="submit" name="submit_1" value="b">Submit B</button>
<button type="submit" name="submit_2" value="c">Submit C</button>
Expand Down