3
declare(strict_types=1);
6
* This file is part of CodeIgniter 4 framework.
8
* (c) CodeIgniter Foundation <admin@codeigniter.com>
10
* For the full copyright and license information, please view
11
* the LICENSE file that was distributed with this source code.
14
namespace CodeIgniter\Test;
16
use BadMethodCallException;
20
use InvalidArgumentException;
23
* Load a response into a DOMDocument for testing assertions based on that
25
* @see \CodeIgniter\Test\DOMParserTest
39
* @throws BadMethodCallException
41
public function __construct()
43
if (! extension_loaded('DOM')) {
44
throw new BadMethodCallException('DOM extension is required, but not currently loaded.'); // @codeCoverageIgnore
47
$this->dom = new DOMDocument('1.0', 'utf-8');
51
* Returns the body of the current document.
53
public function getBody(): string
55
return $this->dom->saveHTML();
59
* Sets a string as the body that we want to work with.
63
public function withString(string $content)
65
// DOMDocument::loadHTML() will treat your string as being in ISO-8859-1
66
// (the HTTP/1.1 default character set) unless you tell it otherwise.
67
// https://stackoverflow.com/a/8218649
68
// So encode characters to HTML numeric string references.
69
$content = mb_encode_numericentity($content, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8');
71
// turning off some errors
72
libxml_use_internal_errors(true);
74
if (! $this->dom->loadHTML($content)) {
75
// unclear how we would get here, given that we are trapping libxml errors
76
// @codeCoverageIgnoreStart
77
libxml_clear_errors();
79
throw new BadMethodCallException('Invalid HTML');
80
// @codeCoverageIgnoreEnd
83
// ignore the whitespace.
84
$this->dom->preserveWhiteSpace = false;
90
* Loads the contents of a file as a string
91
* so that we can work with it.
95
public function withFile(string $path)
97
if (! is_file($path)) {
98
throw new InvalidArgumentException(basename($path) . ' is not a valid file.');
101
$content = file_get_contents($path);
103
return $this->withString($content);
107
* Checks to see if the text is found within the result.
109
public function see(?string $search = null, ?string $element = null): bool
111
// If Element is null, we're just scanning for text
112
if ($element === null) {
113
$content = $this->dom->saveHTML($this->dom->documentElement);
115
return mb_strpos($content, $search) !== false;
118
$result = $this->doXPath($search, $element);
120
return (bool) $result->length;
124
* Checks to see if the text is NOT found within the result.
126
public function dontSee(?string $search = null, ?string $element = null): bool
128
return ! $this->see($search, $element);
132
* Checks to see if an element with the matching CSS specifier
133
* is found within the current DOM.
135
public function seeElement(string $element): bool
137
return $this->see(null, $element);
141
* Checks to see if the element is available within the result.
143
public function dontSeeElement(string $element): bool
145
return $this->dontSee(null, $element);
149
* Determines if a link with the specified text is found
150
* within the results.
152
public function seeLink(string $text, ?string $details = null): bool
154
return $this->see($text, 'a' . $details);
158
* Checks for an input named $field with a value of $value.
160
public function seeInField(string $field, string $value): bool
162
$result = $this->doXPath(null, 'input', ["[@value=\"{$value}\"][@name=\"{$field}\"]"]);
164
return (bool) $result->length;
168
* Checks for checkboxes that are currently checked.
170
public function seeCheckboxIsChecked(string $element): bool
172
$result = $this->doXPath(null, 'input' . $element, [
173
'[@type="checkbox"]',
174
'[@checked="checked"]',
177
return (bool) $result->length;
181
* Checks to see if the XPath can be found.
183
public function seeXPath(string $path): bool
185
$xpath = new DOMXPath($this->dom);
187
return (bool) $xpath->query($path)->length;
191
* Checks to see if the XPath can't be found.
193
public function dontSeeXPath(string $path): bool
195
return ! $this->seeXPath($path);
199
* Search the DOM using an XPath expression.
201
* @return DOMNodeList|false
203
protected function doXPath(?string $search, string $element, array $paths = [])
205
// Otherwise, grab any elements that match
207
$selector = $this->parseSelector($element);
212
if (isset($selector['id'])) {
213
$path = ($selector['tag'] === '')
214
? "id(\"{$selector['id']}\")"
215
: "//{$selector['tag']}[@id=\"{$selector['id']}\"]";
218
elseif (isset($selector['class'])) {
219
$path = ($selector['tag'] === '')
220
? "//*[@class=\"{$selector['class']}\"]"
221
: "//{$selector['tag']}[@class=\"{$selector['class']}\"]";
224
elseif ($selector['tag'] !== '') {
225
$path = "//{$selector['tag']}";
228
if (isset($selector['attr'])) {
229
foreach ($selector['attr'] as $key => $value) {
230
$path .= "[@{$key}=\"{$value}\"]";
234
// $paths might contain a number of different
235
// ready to go xpath portions to tack on.
236
if ($paths !== [] && is_array($paths)) {
237
foreach ($paths as $extra) {
242
if ($search !== null) {
243
$path .= "[contains(., \"{$search}\")]";
246
$xpath = new DOMXPath($this->dom);
248
return $xpath->query($path);
252
* Look for the a selector in the passed text.
254
* @return array{tag: string, id: string|null, class: string|null, attr: array<string, string>|null}
256
public function parseSelector(string $selector)
263
if (str_contains($selector, '#')) {
264
[$tag, $id] = explode('#', $selector);
267
elseif (str_contains($selector, '[') && str_contains($selector, ']')) {
268
$open = strpos($selector, '[');
269
$close = strpos($selector, ']');
271
$tag = substr($selector, 0, $open);
272
$text = substr($selector, $open + 1, $close - 2);
274
// We only support a single attribute currently
275
$text = explode(',', $text);
276
$text = trim(array_shift($text));
278
[$name, $value] = explode('=', $text);
281
$value = trim($value);
282
$attr = [$name => trim($value, '] ')];
285
elseif (str_contains($selector, '.')) {
286
[$tag, $class] = explode('.', $selector);
288
// Otherwise, assume the entire string is our tag