diff --git a/README.md b/README.md index ffdf7e6..16aa6f6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/Browser.php b/src/Browser.php index bc4abec..c5aebe6 100644 --- a/src/Browser.php +++ b/src/Browser.php @@ -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; } diff --git a/src/Browser/Mink/BrowserKitDriver.php b/src/Browser/Mink/BrowserKitDriver.php index 58a742a..6c31286 100644 --- a/src/Browser/Mink/BrowserKitDriver.php +++ b/src/Browser/Mink/BrowserKitDriver.php @@ -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; @@ -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) @@ -489,6 +510,17 @@ 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); @@ -496,11 +528,7 @@ protected function getFormField($xpath) $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]; } /** @@ -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 @@ -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. * diff --git a/src/Browser/Mink/PantherDriver.php b/src/Browser/Mink/PantherDriver.php index 23e9021..ea530a1 100644 --- a/src/Browser/Mink/PantherDriver.php +++ b/src/Browser/Mink/PantherDriver.php @@ -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 diff --git a/tests/BrowserTests.php b/tests/BrowserTests.php index 8628559..318bc41 100644 --- a/tests/BrowserTests.php +++ b/tests/BrowserTests.php @@ -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 */ diff --git a/tests/Fixture/Kernel.php b/tests/Fixture/Kernel.php index 15ee9e3..e5d356a 100644 --- a/tests/Fixture/Kernel.php +++ b/tests/Fixture/Kernel.php @@ -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) )); } diff --git a/tests/Fixture/files/page1.html b/tests/Fixture/files/page1.html index 323fe9d..99d0077 100644 --- a/tests/Fixture/files/page1.html +++ b/tests/Fixture/files/page1.html @@ -52,6 +52,9 @@

h1 title

+ + +