Skip to content

Commit

Permalink
Empty DOM Queries
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
otsch committed Dec 17, 2024
1 parent 4904ed4 commit be8905b
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ vendor
.phpunit.result.cache
.phpunit.cache
/cachedir
/storedir
/tests/_Temp/_cachedir/*
!/tests/_Temp/_cachedir/.gitkeep
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 19 additions & 11 deletions src/Steps/Html/CssSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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('<!doctype html><html></html>'))->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('<!doctype html><html></html>'))->querySelector($query);
} catch (DOMException $exception) {
throw InvalidDomQueryException::fromDomException($query, $exception);
}
}
}

Expand All @@ -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);
}
}
12 changes: 10 additions & 2 deletions src/Steps/Html/XPathQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
46 changes: 46 additions & 0 deletions tests/Steps/HtmlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <<<HTML
<!DOCTYPE html>
<html lang="en">
<head>
<title>Bookstore Example in HTML :)</title>
</head>
<body>
<div id="list">
<div class="element" data-attr="yo">
<a href="/bar">direct element child</a>
<div class="sub-element">
<a href="/baz">sub child</a>
</div>
</div>
</div>
</body>
</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);
});
Expand Down
40 changes: 40 additions & 0 deletions tests/Steps/XmlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <<<XML
<?xml version="1.0" encoding="utf-8"?>
<data>
<items>
<item attr="abc">
<id>123</id>
<subitems>
<subitem>
<id>456</id>
</subitem>
</subitems>
</item>
</items>
</data>
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 = <<<XML
<?xml version="1.0" encoding="utf-8"?>
Expand Down

0 comments on commit be8905b

Please sign in to comment.