From 981c598cdc2ee5e31306d483967fb2834ced7b4a Mon Sep 17 00:00:00 2001 From: otsch Date: Wed, 18 Dec 2024 00:07:22 +0100 Subject: [PATCH] Empty DOM Queries Minor improvement for the `DomQuery` (base for `Dom::cssSelector()` and `Dom::xPath()`): enable providing an empty string as selector, to simply get the node that the selector is applied to. --- .gitignore | 1 + CHANGELOG.md | 4 +++ src/Steps/Html/CssSelector.php | 30 ++++++++++++++-------- src/Steps/Html/XPathQuery.php | 12 +++++++-- tests/Steps/HtmlTest.php | 46 ++++++++++++++++++++++++++++++++++ tests/Steps/XmlTest.php | 40 +++++++++++++++++++++++++++++ 6 files changed, 120 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 6f880a9..b59ac73 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ vendor .phpunit.result.cache .phpunit.cache /cachedir +/storedir /tests/_Temp/_cachedir/* !/tests/_Temp/_cachedir/.gitkeep diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa5d68..b4f6216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### [3.0.4] - 2024-12-18 +### Fixed +* Minor improvement for the `DomQuery` (base for `Dom::cssSelector()` and `Dom::xPath()`): enable providing an empty string as selector, to simply get the node that the selector is applied to. + ### [3.0.3] - 2024-12-11 ### Fixed * Improved fix for non UTF-8 characters in HTML documents declared as UTF-8. diff --git a/src/Steps/Html/CssSelector.php b/src/Steps/Html/CssSelector.php index 3ec1fd8..ced1aed 100644 --- a/src/Steps/Html/CssSelector.php +++ b/src/Steps/Html/CssSelector.php @@ -19,17 +19,21 @@ final class CssSelector extends DomQuery */ public function __construct(string $query) { - if (PhpVersion::isBelow(8, 4)) { - try { - (new CssSelectorConverter())->toXPath($query); - } catch (ExpressionErrorException|SyntaxErrorException $exception) { - throw InvalidDomQueryException::fromSymfonyException($query, $exception); - } - } else { - try { - (new HtmlDocument(''))->querySelector($query); - } catch (DOMException $exception) { - throw InvalidDomQueryException::fromDomException($query, $exception); + $query = trim($query); + + if ($query !== '') { + if (PhpVersion::isBelow(8, 4)) { + try { + (new CssSelectorConverter())->toXPath($query); + } catch (ExpressionErrorException|SyntaxErrorException $exception) { + throw InvalidDomQueryException::fromSymfonyException($query, $exception); + } + } else { + try { + (new HtmlDocument(''))->querySelector($query); + } catch (DOMException $exception) { + throw InvalidDomQueryException::fromDomException($query, $exception); + } } } @@ -38,6 +42,10 @@ public function __construct(string $query) protected function filter(Node $node): NodeList { + if ($this->query === '') { + return new NodeList([$node]); + } + return $node->querySelectorAll($this->query); } } diff --git a/src/Steps/Html/XPathQuery.php b/src/Steps/Html/XPathQuery.php index a8cee67..3a521d8 100644 --- a/src/Steps/Html/XPathQuery.php +++ b/src/Steps/Html/XPathQuery.php @@ -15,13 +15,21 @@ class XPathQuery extends DomQuery */ public function __construct(string $query) { - $this->validateQuery($query); + $query = trim($query); - parent::__construct($query); + if ($query !== '') { + $this->validateQuery($query); + } + + parent::__construct(trim($query)); } protected function filter(Node $node): NodeList { + if ($this->query === '') { + return new NodeList([$node]); + } + return $node->queryXPath($this->query); } diff --git a/tests/Steps/HtmlTest.php b/tests/Steps/HtmlTest.php index a847c35..b00fd8c 100644 --- a/tests/Steps/HtmlTest.php +++ b/tests/Steps/HtmlTest.php @@ -156,6 +156,52 @@ function () { }, ); +test( + 'when selecting elements with each(), you can reference the element already selected within the each() selector ' . + 'itself, in sub selectors', + function () { + $html = << + + + Bookstore Example in HTML :) + + +
+ +
+ + + HTML; + + $response = new RespondedRequest( + new Request('GET', 'https://www.example.com/foo'), + new Response(body: $html), + ); + + $output = helper_invokeStepWithInput( + Html::each('#list .element')->extract([ + // This is what this test is about. The element already selected in each (.element) can be + // referenced in these child selectors. + 'link' => Dom::cssSelector('.element > a')->link(), + 'attribute' => Dom::cssSelector('')->attribute('data-attr'), + ]), + $response, + ); + + expect($output)->toHaveCount(1) + ->and($output[0]->get())->toBe([ + 'link' => 'https://www.example.com/bar', + 'attribute' => 'yo', + ]); + }, +); + test('the static getLink method works without argument', function () { expect(Html::getLink())->toBeInstanceOf(GetLink::class); }); diff --git a/tests/Steps/XmlTest.php b/tests/Steps/XmlTest.php index 29b36da..1d6d24e 100644 --- a/tests/Steps/XmlTest.php +++ b/tests/Steps/XmlTest.php @@ -176,6 +176,46 @@ function () { ]); }); +test( + 'when selecting elements with each(), you can reference the element already selected within the each() selector ' . + 'itself, in sub selectors', + function () { + $xml = << + + + + 123 + + + 456 + + + + + + XML; + + $response = new RespondedRequest( + new Request('GET', 'https://www.example.com/foo'), + new Response(body: $xml), + ); + + $output = helper_invokeStepWithInput( + Xml::each('data items item')->extract([ + // This is what this test is about. The element already selected in each (item) can be + // referenced in these child selectors. + 'id' => Dom::cssSelector('item > id'), + 'attribute' => Dom::cssSelector('')->attribute('attr'), + ]), + $response, + ); + + expect($output)->toHaveCount(1) + ->and($output[0]->get())->toBe(['id' => '123', 'attribute' => 'abc']); + }, +); + it('works with tags with camelCase names', function () { $xml = <<