Skip to content

Commit

Permalink
[feature] add ability to attach multiple files (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
kbond authored Aug 24, 2021
1 parent 4739145 commit 34b55fa
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 39 deletions.
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

0 comments on commit 34b55fa

Please sign in to comment.