diff --git a/STEPS.md b/STEPS.md
index 03143ed7..b2e242c7 100644
--- a/STEPS.md
+++ b/STEPS.md
@@ -2944,6 +2944,132 @@ Then the XML should not use the namespace "http://example.com/nonexistent"
+
+ @Then the response should match the following XSD schema:
+
+
+Assert that the response validates against an inline XSD schema
+
+
+```gherkin
+Then the response should match the following XSD schema:
+ """
+
+
+
+
+ """
+
+```
+
+
+
+
+ @Then the response should match the XSD schema in the file :filename
+
+
+Assert that the response validates against an XSD schema from a file
+
+
+```gherkin
+Then the response should match the XSD schema in the file "xml_schema.xsd"
+
+```
+
+
+
+
+ @Then the response should match the following DTD:
+
+
+Assert that the response validates against an inline DTD
+
+
+```gherkin
+Then the response should match the following DTD:
+ """
+
+ """
+
+```
+
+
+
+
+ @Then the response should match the DTD in the file :filename
+
+
+Assert that the response validates against a DTD from a file
+
+
+```gherkin
+Then the response should match the DTD in the file "xml_schema.dtd"
+
+```
+
+
+
+
+ @Then the response should match the following RelaxNG schema:
+
+
+Assert that the response validates against an inline RelaxNG schema
+
+
+```gherkin
+Then the response should match the following RelaxNG schema:
+ """
+
+
+
+ """
+
+```
+
+
+
+
+ @Then the response should match the RelaxNG schema in the file :filename
+
+
+Assert that the response validates against a RelaxNG schema from a file
+
+
+```gherkin
+Then the response should match the RelaxNG schema in the file "xml_schema.rng"
+
+```
+
+
+
+
+ @Then the response should be a valid RSS feed
+
+
+Assert that the response is a valid RSS 2.0 feed
+
+
+```gherkin
+Then the response should be a valid RSS feed
+
+```
+
+
+
+
+ @Then the response should be a valid Atom feed
+
+
+Assert that the response is a valid Atom feed
+
+
+```gherkin
+Then the response should be a valid Atom feed
+
+```
+
+
+
## Drupal\BlockTrait
diff --git a/src/XmlTrait.php b/src/XmlTrait.php
index 0cb4c0c0..4c8a2c15 100644
--- a/src/XmlTrait.php
+++ b/src/XmlTrait.php
@@ -81,21 +81,7 @@ public function xmlAfterScenario(): void {
*/
#[Given('the response content from the file :filename')]
public function xmlSetResponseContentFromFile(string $filename): void {
- $files_path = rtrim((string) $this->getMinkParameter('files_path'), '/');
- $file_path = $files_path . '/' . $filename;
-
- if (!file_exists($file_path)) {
- throw new \RuntimeException(sprintf('The file "%s" does not exist.', $file_path));
- }
-
- $content = file_get_contents($file_path);
- if ($content === FALSE) {
- // @codeCoverageIgnoreStart
- throw new \RuntimeException(sprintf('Failed to read the file "%s".', $file_path));
- // @codeCoverageIgnoreEnd
- }
-
- $this->xmlTestContent = $content;
+ $this->xmlTestContent = $this->xmlReadFile($filename);
$this->xmlDocument = NULL;
$this->xmlXpath = NULL;
$this->xmlContentHash = NULL;
@@ -551,6 +537,130 @@ public function xmlAssertNamespaceNotExists(string $namespace): void {
}
}
+ /**
+ * Assert that the response validates against an inline XSD schema.
+ *
+ * @code
+ * Then the response should match the following XSD schema:
+ * """
+ *
+ *
+ *
+ *
+ * """
+ * @endcode
+ */
+ #[Then('the response should match the following XSD schema:')]
+ public function xmlAssertMatchesXsd(PyStringNode $schema): void {
+ $this->xmlValidateXsd($schema->getRaw());
+ }
+
+ /**
+ * Assert that the response validates against an XSD schema from a file.
+ *
+ * @code
+ * Then the response should match the XSD schema in the file "xml_schema.xsd"
+ * @endcode
+ */
+ #[Then('the response should match the XSD schema in the file :filename')]
+ public function xmlAssertMatchesXsdFromFile(string $filename): void {
+ $this->xmlValidateXsd($this->xmlReadFile($filename));
+ }
+
+ /**
+ * Assert that the response validates against an inline DTD.
+ *
+ * DTDs are namespace-unaware: to validate a namespaced document, the DTD must
+ * declare the prefixed element names and the `xmlns` attributes.
+ *
+ * @code
+ * Then the response should match the following DTD:
+ * """
+ *
+ * """
+ * @endcode
+ */
+ #[Then('the response should match the following DTD:')]
+ public function xmlAssertMatchesDtd(PyStringNode $dtd): void {
+ $this->xmlValidateDtd($dtd->getRaw());
+ }
+
+ /**
+ * Assert that the response validates against a DTD from a file.
+ *
+ * DTDs are namespace-unaware: to validate a namespaced document, the DTD must
+ * declare the prefixed element names and the `xmlns` attributes.
+ *
+ * @code
+ * Then the response should match the DTD in the file "xml_schema.dtd"
+ * @endcode
+ */
+ #[Then('the response should match the DTD in the file :filename')]
+ public function xmlAssertMatchesDtdFromFile(string $filename): void {
+ $this->xmlValidateDtd($this->xmlReadFile($filename));
+ }
+
+ /**
+ * Assert that the response validates against an inline RelaxNG schema.
+ *
+ * @code
+ * Then the response should match the following RelaxNG schema:
+ * """
+ *
+ *
+ *
+ * """
+ * @endcode
+ */
+ #[Then('the response should match the following RelaxNG schema:')]
+ public function xmlAssertMatchesRelaxNg(PyStringNode $schema): void {
+ $this->xmlValidateRelaxNg($schema->getRaw());
+ }
+
+ /**
+ * Assert that the response validates against a RelaxNG schema from a file.
+ *
+ * @code
+ * Then the response should match the RelaxNG schema in the file "xml_schema.rng"
+ * @endcode
+ */
+ #[Then('the response should match the RelaxNG schema in the file :filename')]
+ public function xmlAssertMatchesRelaxNgFromFile(string $filename): void {
+ $this->xmlValidateRelaxNg($this->xmlReadFile($filename));
+ }
+
+ /**
+ * Assert that the response is a valid RSS 2.0 feed.
+ *
+ * Checks the required RSS 2.0 structure: an `rss` root with a `version` of
+ * `2.0`, a single `channel` with `title`, `link` and `description`, and an
+ * `item` with at least a `title` or a `description`.
+ *
+ * @code
+ * Then the response should be a valid RSS feed
+ * @endcode
+ */
+ #[Then('the response should be a valid RSS feed')]
+ public function xmlAssertValidRssFeed(): void {
+ $this->xmlValidateRssFeed();
+ }
+
+ /**
+ * Assert that the response is a valid Atom feed.
+ *
+ * Checks the required Atom structure: a `feed` root in the Atom namespace
+ * with `id`, `title` and `updated`, and each `entry` with `id`, `title`
+ * and `updated`.
+ *
+ * @code
+ * Then the response should be a valid Atom feed
+ * @endcode
+ */
+ #[Then('the response should be a valid Atom feed')]
+ public function xmlAssertValidAtomFeed(): void {
+ $this->xmlValidateAtomFeed();
+ }
+
/**
* Print the last XML response.
*
@@ -683,4 +793,210 @@ protected function xmlFormatErrors(array $errors): string {
return implode('; ', $messages);
}
+ /**
+ * Read a fixture file's contents.
+ *
+ * @param string $filename
+ * The fixture file name relative to the Mink files path.
+ *
+ * @return string
+ * The file contents.
+ */
+ protected function xmlReadFile(string $filename): string {
+ $files_path = rtrim((string) $this->getMinkParameter('files_path'), '/');
+ $file_path = $files_path . '/' . $filename;
+
+ if (!file_exists($file_path)) {
+ throw new \RuntimeException(sprintf('The file "%s" does not exist.', $file_path));
+ }
+
+ $content = file_get_contents($file_path);
+ if ($content === FALSE) {
+ // @codeCoverageIgnoreStart
+ throw new \RuntimeException(sprintf('Failed to read the file "%s".', $file_path));
+ // @codeCoverageIgnoreEnd
+ }
+
+ return $content;
+ }
+
+ /**
+ * Validate the response against an XSD schema.
+ *
+ * @param string $schema
+ * The XSD schema source.
+ */
+ protected function xmlValidateXsd(string $schema): void {
+ $this->xmlEnsureDocument();
+
+ libxml_clear_errors();
+ $valid = @$this->xmlDocument->schemaValidateSource($schema);
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+
+ if (!$valid) {
+ throw new ExpectationException(sprintf('The response does not match the XSD schema: %s', $this->xmlFormatErrors($errors)), $this->getSession()->getDriver());
+ }
+ }
+
+ /**
+ * Validate the response against a RelaxNG schema.
+ *
+ * @param string $schema
+ * The RelaxNG schema source.
+ */
+ protected function xmlValidateRelaxNg(string $schema): void {
+ $this->xmlEnsureDocument();
+
+ libxml_clear_errors();
+ $valid = @$this->xmlDocument->relaxNGValidateSource($schema);
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+
+ if (!$valid) {
+ throw new ExpectationException(sprintf('The response does not match the RelaxNG schema: %s', $this->xmlFormatErrors($errors)), $this->getSession()->getDriver());
+ }
+ }
+
+ /**
+ * Validate the response against a DTD.
+ *
+ * The DTD is embedded as an internal subset and the response reloaded with
+ * validation enabled, so a DTD from a file and an inline DTD share one code
+ * path without exposing external-entity loading.
+ *
+ * DTDs are namespace-unaware, so a namespaced response is validated verbatim
+ * and its `xmlns` attributes must be declared in the DTD, matching native DTD
+ * validation semantics.
+ *
+ * @param string $dtd
+ * The DTD source (element, attribute and entity declarations).
+ */
+ protected function xmlValidateDtd(string $dtd): void {
+ $this->xmlEnsureDocument();
+
+ $root = $this->xmlDocument->documentElement;
+ if (!$root instanceof \DOMElement) {
+ // @codeCoverageIgnoreStart
+ throw new ExpectationException('The response has no root element to validate against the DTD.', $this->getSession()->getDriver());
+ // @codeCoverageIgnoreEnd
+ }
+
+ $body = $this->xmlDocument->saveXML($root);
+ if ($body === FALSE) {
+ // @codeCoverageIgnoreStart
+ throw new ExpectationException('Failed to serialise the response for DTD validation.', $this->getSession()->getDriver());
+ // @codeCoverageIgnoreEnd
+ }
+
+ $combined = sprintf("\n\n%s", $root->nodeName, $dtd, $body);
+
+ $document = new \DOMDocument();
+ libxml_clear_errors();
+ $loaded = @$document->loadXML($combined, LIBXML_DTDVALID);
+ $errors = libxml_get_errors();
+ libxml_clear_errors();
+
+ if (!$loaded || $errors !== []) {
+ throw new ExpectationException(sprintf('The response does not match the DTD: %s', $this->xmlFormatErrors($errors)), $this->getSession()->getDriver());
+ }
+ }
+
+ /**
+ * Validate the response as an RSS 2.0 feed.
+ */
+ protected function xmlValidateRssFeed(): void {
+ $this->xmlEnsureDocument();
+
+ $root = $this->xmlDocument->documentElement;
+ if (!$root instanceof \DOMElement || $root->localName !== 'rss') {
+ throw new ExpectationException('The response is not a valid RSS feed: the root element must be "rss".', $this->getSession()->getDriver());
+ }
+
+ if ($root->getAttribute('version') !== '2.0') {
+ throw new ExpectationException('The response is not a valid RSS feed: the "rss" element must have a "version" attribute of "2.0".', $this->getSession()->getDriver());
+ }
+
+ $channels = $this->xmlDirectChildElements($root, 'channel');
+ if (count($channels) !== 1) {
+ throw new ExpectationException('The response is not a valid RSS feed: the "rss" element must contain exactly one "channel" element.', $this->getSession()->getDriver());
+ }
+
+ $channel = $channels[0];
+
+ foreach (['title', 'link', 'description'] as $required) {
+ if ($this->xmlDirectChildElements($channel, $required) === []) {
+ throw new ExpectationException(sprintf('The response is not a valid RSS feed: the "channel" element is missing the required "%s" element.', $required), $this->getSession()->getDriver());
+ }
+ }
+
+ foreach ($this->xmlDirectChildElements($channel, 'item') as $item) {
+ $has_title = $this->xmlDirectChildElements($item, 'title') !== [];
+ $has_description = $this->xmlDirectChildElements($item, 'description') !== [];
+
+ if (!$has_title && !$has_description) {
+ throw new ExpectationException('The response is not a valid RSS feed: each "item" element must contain a "title" or a "description" element.', $this->getSession()->getDriver());
+ }
+ }
+ }
+
+ /**
+ * Validate the response as an Atom feed.
+ */
+ protected function xmlValidateAtomFeed(): void {
+ $this->xmlEnsureDocument();
+
+ $namespace = 'http://www.w3.org/2005/Atom';
+
+ $root = $this->xmlDocument->documentElement;
+ if (!$root instanceof \DOMElement || $root->localName !== 'feed' || $root->namespaceURI !== $namespace) {
+ throw new ExpectationException('The response is not a valid Atom feed: the root element must be "feed" in the Atom namespace.', $this->getSession()->getDriver());
+ }
+
+ foreach (['id', 'title', 'updated'] as $required) {
+ if ($this->xmlDirectChildElements($root, $required, $namespace) === []) {
+ throw new ExpectationException(sprintf('The response is not a valid Atom feed: the "feed" element is missing the required "%s" element.', $required), $this->getSession()->getDriver());
+ }
+ }
+
+ foreach ($this->xmlDirectChildElements($root, 'entry', $namespace) as $entry) {
+ foreach (['id', 'title', 'updated'] as $required) {
+ if ($this->xmlDirectChildElements($entry, $required, $namespace) === []) {
+ throw new ExpectationException(sprintf('The response is not a valid Atom feed: an "entry" element is missing the required "%s" element.', $required), $this->getSession()->getDriver());
+ }
+ }
+ }
+ }
+
+ /**
+ * Get direct child elements matching a local name and optional namespace.
+ *
+ * @param \DOMNode $parent
+ * The parent node.
+ * @param string $name
+ * The local element name to match.
+ * @param string|null $namespace
+ * The namespace URI to match, or NULL to match elements in any namespace.
+ *
+ * @return array
+ * The matching direct child elements.
+ */
+ protected function xmlDirectChildElements(\DOMNode $parent, string $name, ?string $namespace = NULL): array {
+ $matches = [];
+
+ foreach ($parent->childNodes as $child) {
+ if (!$child instanceof \DOMElement || $child->localName !== $name) {
+ continue;
+ }
+
+ if ($namespace !== NULL && $child->namespaceURI !== $namespace) {
+ continue;
+ }
+
+ $matches[] = $child;
+ }
+
+ return $matches;
+ }
+
}
diff --git a/tests/behat/features/xml.feature b/tests/behat/features/xml.feature
index 46f6cce6..259be5b4 100644
--- a/tests/behat/features/xml.feature
+++ b/tests/behat/features/xml.feature
@@ -647,3 +647,264 @@ Feature: Check that XmlTrait works
"""
Unable to access the response before visiting a page
"""
+
+ Scenario: Assert "Then the response should match the following XSD schema:" works
+ Given the response content is the following:
+ """
+ World
+ """
+ Then the response should match the following XSD schema:
+ """
+
+
+
+
+
+
+
+
+
+
+ """
+
+ Scenario: Assert "Then the response should match the XSD schema in the file :filename" works
+ When I go to "/sites/default/files/xml_valid.xml"
+ Then the response should match the XSD schema in the file "xml_schema.xsd"
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should match the XSD schema in the file :filename" fails with an error
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_simple.xml"
+ Then the response should match the XSD schema in the file "xml_schema.xsd"
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ The response does not match the XSD schema
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that "Then the response should match the XSD schema in the file :filename" fails with an exception for missing file
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ Then the response should match the XSD schema in the file "nonexistent.xsd"
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an exception:
+ """
+ does not exist
+ """
+
+ Scenario: Assert "Then the response should match the following DTD:" works
+ Given the response content is the following:
+ """
+ Hello
+ """
+ Then the response should match the following DTD:
+ """
+
+ """
+
+ Scenario: Assert "Then the response should match the DTD in the file :filename" works
+ When I go to "/sites/default/files/xml_valid.xml"
+ Then the response should match the DTD in the file "xml_schema.dtd"
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should match the DTD in the file :filename" fails with an error
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_simple.xml"
+ Then the response should match the DTD in the file "xml_schema.dtd"
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ The response does not match the DTD
+ """
+
+ Scenario: Assert "Then the response should match the following RelaxNG schema:" works
+ Given the response content is the following:
+ """
+ Hello
+ """
+ Then the response should match the following RelaxNG schema:
+ """
+
+
+
+ """
+
+ Scenario: Assert "Then the response should match the RelaxNG schema in the file :filename" works
+ When I go to "/sites/default/files/xml_valid.xml"
+ Then the response should match the RelaxNG schema in the file "xml_schema.rng"
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should match the RelaxNG schema in the file :filename" fails with an error
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_simple.xml"
+ Then the response should match the RelaxNG schema in the file "xml_schema.rng"
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ The response does not match the RelaxNG schema
+ """
+
+ Scenario: Assert "Then the response should be a valid RSS feed" works
+ When I go to "/sites/default/files/rss_valid.xml"
+ Then the response should be a valid RSS feed
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid RSS feed" fails with an error for a non-rss root
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+
+ '''
+ Then the response should be a valid RSS feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ the root element must be "rss"
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid RSS feed" fails with an error for a wrong version
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+ tld
+ '''
+ Then the response should be a valid RSS feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ must have a "version" attribute of "2.0"
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid RSS feed" fails with an error for a missing channel
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+
+ '''
+ Then the response should be a valid RSS feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ must contain exactly one "channel" element
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid RSS feed" fails with an error for a missing required channel element
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+ tl
+ '''
+ Then the response should be a valid RSS feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ is missing the required "description" element
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid RSS feed" fails with an error for an item without a title or description
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+ tldx
+ '''
+ Then the response should be a valid RSS feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ each "item" element must contain a "title" or a "description"
+ """
+
+ Scenario: Assert "Then the response should be a valid Atom feed" works
+ When I go to "/sites/default/files/atom_valid.xml"
+ Then the response should be a valid Atom feed
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid Atom feed" fails with an error for a non-atom root
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+ xtu
+ '''
+ Then the response should be a valid Atom feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ the root element must be "feed" in the Atom namespace
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid Atom feed" fails with an error for a missing required feed element
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+ xt
+ '''
+ Then the response should be a valid Atom feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ the "feed" element is missing the required "updated" element
+ """
+
+ @trait:XmlTrait
+ Scenario: Assert that negative assertion for "Then the response should be a valid Atom feed" fails with an error for an entry missing a required element
+ Given some behat configuration
+ And scenario steps:
+ """
+ When I go to "/sites/default/files/xml_valid.xml"
+ And the response content is the following:
+ '''
+ xtueet2024
+ '''
+ Then the response should be a valid Atom feed
+ """
+ When I run "behat --no-colors"
+ Then it should fail with an error:
+ """
+ an "entry" element is missing the required "updated" element
+ """
diff --git a/tests/behat/fixtures/atom_valid.xml b/tests/behat/fixtures/atom_valid.xml
new file mode 100644
index 00000000..657e4a68
--- /dev/null
+++ b/tests/behat/fixtures/atom_valid.xml
@@ -0,0 +1,16 @@
+
+
+ urn:uuid:example-feed
+ Example Feed
+ 2024-01-01T00:00:00Z
+
+ urn:uuid:example-entry-1
+ First entry
+ 2024-01-01T00:00:00Z
+
+
+ urn:uuid:example-entry-2
+ Second entry
+ 2024-01-02T00:00:00Z
+
+
diff --git a/tests/behat/fixtures/rss_valid.xml b/tests/behat/fixtures/rss_valid.xml
new file mode 100644
index 00000000..606e05c5
--- /dev/null
+++ b/tests/behat/fixtures/rss_valid.xml
@@ -0,0 +1,17 @@
+
+
+
+ Example Feed
+ https://example.com/
+ An example RSS 2.0 feed.
+
+ First item
+ https://example.com/first
+ The first item description.
+
+
+ Second item
+ https://example.com/second
+
+
+
diff --git a/tests/behat/fixtures/xml_schema.dtd b/tests/behat/fixtures/xml_schema.dtd
new file mode 100644
index 00000000..aba7ee1d
--- /dev/null
+++ b/tests/behat/fixtures/xml_schema.dtd
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/tests/behat/fixtures/xml_schema.rng b/tests/behat/fixtures/xml_schema.rng
new file mode 100644
index 00000000..ce980868
--- /dev/null
+++ b/tests/behat/fixtures/xml_schema.rng
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/behat/fixtures/xml_schema.xsd b/tests/behat/fixtures/xml_schema.xsd
new file mode 100644
index 00000000..6ba85b3b
--- /dev/null
+++ b/tests/behat/fixtures/xml_schema.xsd
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+