+
+HTML;
+
+ public function testPresenceOfSelectors()
+ {
+ $this->assertContainsSelector('main', $this->markup);
+ $this->assertContainsSelector('h1', $this->markup);
+ $this->assertContainsSelector('a.link', $this->markup);
+ $this->assertContainsSelector('main p', $this->markup);
+ $this->assertContainsSelector('h1 + p', $this->markup);
+ $this->assertContainsSelector('p > a', $this->markup);
+ $this->assertContainsSelector('a[href$="example.com"]', $this->markup);
+
+ $this->assertNotContainsSelector('h2', $this->markup);
+ $this->assertNotContainsSelector('a[href="https://example.org"]', $this->markup);
+ $this->assertNotContainsSelector('p main', $this->markup);
+
+ $this->assertSelectorCount(0, 'h2', $this->markup);
+ $this->assertSelectorCount(1, 'h1', $this->markup);
+ $this->assertSelectorCount(2, 'p', $this->markup);
+
+ $this->assertHasElementWithAttributes(['href' => 'https://example.com'], $this->markup);
+
+ $this->assertNotHasElementWithAttributes(
+ ['href' => 'https://example.org'],
+ $this->markup,
+ 'URL uses .com, not .org.'
+ );
+ }
+
+ public function testMatchingContentsOfSelectors()
+ {
+ $this->assertElementContains('Good news', 'main', $this->markup);
+ $this->assertElementContains('Good news', 'h1', $this->markup);
+ $this->assertElementContains('#TestEverything', 'p', $this->markup);
+ $this->assertElementContains('class="link"', 'p', $this->markup);
+ $this->assertElementContains('#TestEverything', 'main *:last-child', $this->markup);
+
+ $this->assertElementNotContains('good news', 'h1', $this->markup, 'Case-sensitive by default.');
+ $this->assertElementNotContains(
+ '#TestEverything',
+ 'p:first-child',
+ $this->markup,
+ '#TestEverything is in the second paragraph'
+ );
+ $this->assertElementNotContains(
+ 'class="link"',
+ 'a',
+ $this->markup,
+ 'The class is part of the outer, not inner HTML'
+ );
+
+ $this->assertElementRegExp('/\w+ news/', 'h1', $this->markup);
+ $this->assertElementRegExp('/GOOD NEWS/i', 'h1', $this->markup);
+ $this->assertElementRegExp('/latest reports/', 'p > a', $this->markup);
+
+ $this->assertElementNotRegExp(
+ '/\w+ news/',
+ 'p',
+ $this->markup,
+ 'This text is in the heading, not the paragraph.'
+ );
+ $this->assertElementNotRegExp(
+ '/#TESTEVERYTHING/',
+ 'p',
+ $this->markup,
+ 'No case-insensitive flag'
+ );
+ }
+}
diff --git a/tests/MarkupAssertionsTraitTest.php b/tests/MarkupAssertionsTraitTest.php
deleted file mode 100644
index 0cacc7f..0000000
--- a/tests/MarkupAssertionsTraitTest.php
+++ /dev/null
@@ -1,381 +0,0 @@
-assertContainsSelector(
- $selector,
- 'Example'
- );
- }
-
- /**
- * @test
- * @testdox assertContainsSelector() should pick up multiple instances of a selector
- */
- public function assertContainsSelector_should_pick_up_multiple_instances()
- {
- $this->assertContainsSelector(
- 'a',
- 'Home | About | Contact'
- );
- }
-
- /**
- * @test
- * @testdox assertNotContainsSelector() should verify that the given selector does not exist
- * @dataProvider provideSelectorVariants
- */
- public function assertNotContainsSelector_should_verify_that_the_given_selector_does_not_exist($selector)
- {
- $this->assertNotContainsSelector(
- $selector,
- '
This element has little to do with the link.
'
- );
- }
-
- /**
- * @test
- * @testdox assertSelectorCount() should count the instances of a selector
- */
- public function assertSelectorCount_should_count_the_number_of_instances()
- {
- $this->assertSelectorCount(
- 3,
- 'li',
- '
1
2
3
'
- );
- }
-
- /**
- * @test
- * @testdox assertHasElementWithAttributes() should find an element with the given attributes
- */
- public function assertHasElementWithAttributes_should_find_elements_with_matching_attributes()
- {
- $this->assertHasElementWithAttributes(
- [
- 'type' => 'email',
- 'value' => 'test@example.com',
- ],
- ' '
- );
- }
-
- /**
- * @test
- * @testdox assertHasElementWithAttributes() should be able to parse spaces in attribute values
- * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/13
- */
- public function assertHasElementWithAttributes_should_be_able_to_handle_spaces()
- {
- $this->assertHasElementWithAttributes(
- [
- 'data-attr' => 'foo bar baz',
- ],
- '
Contents
'
- );
- }
-
- /**
- * @test
- * @testdox assertNotHasElementWithAttributes() should ensure no element has the provided attributes
- */
- public function assertNotHasElementWithAttributes_should_find_no_elements_with_matching_attributes()
- {
- $this->assertNotHasElementWithAttributes(
- [
- 'type' => 'email',
- 'value' => 'test@example.com',
- ],
- ' '
- );
- }
-
- /**
- * @test
- * @testdox assertElementContains() should be able to search for a selector
- */
- public function assertElementContains_can_match_a_selector()
- {
- $this->assertElementContains(
- 'ipsum',
- '#main',
- 'Lorem ipsum
Lorem ipsum
'
- );
- }
-
- /**
- * @test
- * @testdox assertElementContains() should be able to chain multiple selectors
- */
- public function assertElementContains_can_chain_multiple_selectors()
- {
- $this->assertElementContains(
- 'ipsum',
- '#main .foo',
- '
Lorem ipsum
'
- );
- }
-
- /**
- * @test
- * @testdox assertElementContains() should scope text to the selected element
- */
- public function assertElementContains_should_scope_matches_to_selector()
- {
- $this->expectException(AssertionFailedError::class);
- $this->expectExceptionMessage('The #main div does not contain the string "ipsum".');
-
- $this->assertElementContains(
- 'ipsum',
- '#main',
- 'Lorem ipsum
Foo bar baz
',
- 'The #main div does not contain the string "ipsum".'
- );
- }
-
- /**
- * @test
- * @testdox assertElementContains() should handle various character sets
- * @dataProvider provideGreetingsInDifferentLanguages
- * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/31
- */
- public function assertElementContains_should_handle_various_character_sets($greeting)
- {
- $this->assertElementContains(
- $greeting,
- 'h1',
- sprintf('
%s
', $greeting)
- );
- }
-
- /**
- * @test
- * @testdox assertElementNotContains() should be able to search for a selector
- */
- public function assertElementNotContains_can_match_a_selector()
- {
- $this->assertElementNotContains(
- 'ipsum',
- '#main',
- '
Foo bar baz
Some string
'
- );
- }
-
- /**
- * @test
- * @testdox assertElementNotContains() should handle various character sets
- * @dataProvider provideGreetingsInDifferentLanguages
- * @ticket https://github.com/stevegrunwell/phpunit-markup-assertions/issues/31
- */
- public function assertElementNotContains_should_handle_various_character_sets($greeting)
- {
- $this->assertElementNotContains(
- $greeting,
- 'h1',
- sprintf('
Translation
%s
', $greeting)
- );
- }
-
- /**
- * @test
- * @testdox assertElementRegExp() should use regular expression matching
- */
- public function assertElementRegExp_should_use_regular_expression_matching()
- {
- $this->assertElementRegExp(
- '/[A-Z0-9-]+/',
- '#main',
- 'Lorem ipsum
ABC123
'
- );
- }
-
- /**
- * @test
- * @testdox assertElementRegExp() should be able to search for nested contents
- */
- public function assertElementRegExp_should_be_able_to_match_nested_contents()
- {
- $this->assertElementRegExp(
- '/[A-Z]+/',
- '#main',
- 'Lorem ipsum
ABC
'
- );
- }
-
- /**
- * @test
- * @testdox assertElementNotRegExp() should use regular expression matching
- */
- public function testAssertElementNotRegExp()
- {
- $this->assertElementNotRegExp(
- '/[0-9-]+/',
- '#main',
- 'Foo bar baz
ABC
'
- );
- }
-
-
- /**
- * @test
- * @testdox flattenAttributeArray() should flatten an array of attributes
- * @dataProvider provideAttributes
- */
- public function flattenArrayAttribute_should_flatten_arrays_of_attributes($attributes, $expected)
- {
- $method = new \ReflectionMethod($this, 'flattenAttributeArray');
- $method->setAccessible(true);
-
- $this->assertSame($expected, $method->invoke($this, $attributes));
- }
-
- /**
- * @test
- * @testdox flattenAttributeArray() should throw a RiskyTestError if the array is empty
- * @dataProvider provideAttributes
- */
- public function flattenAttributeArray_should_throw_a_RiskyTestError_if_given_an_empty_array()
- {
- $this->expectException(RiskyTestError::class);
-
- $method = new \ReflectionMethod($this, 'flattenAttributeArray');
- $method->setAccessible(true);
- $method->invoke($this, []);
- }
-
- /**
- * @test
- * @testdox getInnerHtmlOfMatchedElements() should retrieve the inner HTML
- * @dataProvider provideInnerHtml
- */
- public function getInnerHtmlOfMatchedElements_should_retrieve_the_inner_HTML($markup, $selector, $expected)
- {
- $method = new \ReflectionMethod($this, 'getInnerHtmlOfMatchedElements');
- $method->setAccessible(true);
-
- $this->assertEquals($expected, $method->invoke($this, $markup, $selector));
- }
-
- /**
- * Data provider for testFlattenAttributeArray().
- */
- public function provideAttributes()
- {
- return [
- 'Single attribute' => [
- [
- 'id' => 'first-name',
- ],
- '[id="first-name"]',
- ],
- 'Multiple attributes' => [
- [
- 'id' => 'first-name',
- 'value' => 'Ringo',
- ],
- '[id="first-name"][value="Ringo"]',
- ],
- 'Boolean attribute' => [
- [
- 'checked' => null,
- ],
- '[checked]',
- ],
- 'Data attribute' => [
- [
- 'data-foo' => 'bar',
- ],
- '[data-foo="bar"]',
- ],
- 'Value contains quotes' => [
- [
- 'name' => 'Austin "Danger" Powers',
- ],
- '[name="Austin "Danger" Powers"]',
- ],
- ];
- }
-
- /**
- * Data provider for testGetInnerHtmlOfMatchedElements().
- *
- * @return array>
- */
- public function provideInnerHtml()
- {
- return [
- 'A single match' => [
- 'Foo bar baz',
- 'body',
- 'Foo bar baz',
- ],
- 'Multiple matching elements' => [
- '
';
+
+ $this->assertFalse(
+ $constraint->evaluate($html, '', true),
+ 'The string "class" does not appear in either paragraph, and thus should not be matched.'
+ );
+ }
+
+ /**
+ * @test
+ */
+ public function it_should_fail_with_a_useful_error_message()
+ {
+ $html = '
Some other string
';
+ $expected = <<<'MSG'
+Failed asserting that element matching selector 'p' contains string 'some string'.
+Matching element:
+[
+
';
+
+ $this->assertFalse(
+ $constraint->evaluate($html, '', true),
+ '"12345" does not match pattern /[a-z]/'
+ );
+ }
+
+ /**
+ * @test
+ */
+ public function it_should_test_against_the_inner_contents_of_the_found_nodes()
+ {
+ $constraint = new ElementMatchesRegExp(new Selector('p'), '/class/');
+ $html = '
First
Second
';
+
+ $this->assertFalse(
+ $constraint->evaluate($html, '', true),
+ 'The string "class" does not appear in either paragraph, and thus should not be matched.'
+ );
+ }
+
+ /**
+ * @test
+ */
+ public function it_should_scope_queries_to_the_selector()
+ {
+ $constraint = new ElementMatchesRegExp(new Selector('h1'), '/\d+/');
+ $html = '
',
+ new Selector('li'),
+ 3
+ ];
+
+ yield 'Nested elements with low specificity' => [
+ '
Hello
There
',
+ new Selector('div'),
+ 2,
+ ];
+
+ yield 'Nested elements with high-specificity' => [
+ '
Hello
There
',
+ new Selector('div>div'),
+ 1,
+ ];
+ }
+}
diff --git a/tests/Unit/DOMTest.php b/tests/Unit/DOMTest.php
new file mode 100644
index 0000000..f077205
--- /dev/null
+++ b/tests/Unit/DOMTest.php
@@ -0,0 +1,143 @@
+assertSame($expected, $dom->countInstancesOfSelector($selector));
+ }
+
+ /**
+ * @test
+ * @testdox getInnerHtml() should retrieve the inner HTML for each matching element.
+ */
+ public function getInnerHtml_should_retrieve_the_inner_HTML_for_each_matching_element()
+ {
+ $markup = <<<'HTML'
+
+
The strong element
+
The em element
+
The kbd element
+
+HTML;
+ $dom = new DOM($markup);
+
+ $this->assertSame(
+ [
+ 'The strong element',
+ 'The em element',
+ 'The kbd element',
+ ],
+ $dom->getInnerHtml(new Selector('li'))
+ );
+ }
+
+ /**
+ * @test
+ * @testdox getInnerHtml() should return an empty array if there are no matches
+ */
+ public function getInnerHtml_should_return_an_empty_array_if_there_are_no_matches()
+ {
+ $dom = new DOM('
A title
');
+
+ $this->assertEmpty($dom->getInnerHtml(new Selector('h2')));
+ }
+
+ /**
+ * @test
+ * @testdox getOuterHtml() should retrieve the outer HTML for each matching element.
+ */
+ public function getOuterHtml_should_retrieve_the_outer_HTML_for_each_matching_element()
+ {
+ $markup = <<<'HTML'
+
',
+ ],
+ $dom->getOuterHtml(new Selector('li'))
+ );
+ }
+
+ /**
+ * @test
+ * @testdox getOuterHtml() should return an empty array if there are no matches
+ */
+ public function getOuterHtml_should_return_an_empty_array_if_there_are_no_matches()
+ {
+ $dom = new DOM('
A title
');
+
+ $this->assertEmpty($dom->getOuterHtml(new Selector('h2')));
+ }
+
+ /**
+ * @test
+ * @testdox query() should throw a SelectorException if the selector is invalid
+ */
+ public function query_should_throw_a_SelectorException_if_the_selector_is_invalid()
+ {
+ $dom = new DOM('
Some markup
');
+ $selector = new Selector('#');
+
+ try {
+ $dom->query($selector);
+ } catch (\Exception $e) {
+ $this->assertInstanceOf(SelectorException::class, $e);
+ return;
+ }
+
+ $this->fail('Failed to catch a SelectorException from invalid selector "#".');
+ }
+
+ /**
+ * Return test cases with varying numbers of .inner elements.
+ *
+ * @return iterable
+ */
+ public function provideMarkupWithInnerClass()
+ {
+ yield 'No matches' => [
+ '',
+ 0,
+ ];
+
+ yield 'One match' => [
+ '