This commit is contained in:
2020-03-27 10:13:51 +07:00
commit da1024a5b3
16614 changed files with 3274282 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
<?php
namespace Codeception\Util;
use Codeception\Step\Action;
/**
* Class for defining an array actions to be executed inside `performOn` of WebDriver
*
* ```php
* <?php
* (new ActionSequence)->click('do')->click('undo');
* ActionSequence::build()->click('do')->click('undo');
* ```
*
* @method $this see([optional])
* @method $this dontSee([optional])
* @method $this seeElement([optional])
* @method $this dontSeeElement([optional])
* @method $this click([optional])
* @method $this wait([optional])
* @method $this waitForElementChange([optional])
* @method $this waitForElement([optional])
* @method $this waitForElementVisible([optional])
* @method $this waitForElementNotVisible([optional])
* @method $this waitForText([optional])
* @method $this submitForm([optional])
* @method $this seeLink([optional])
* @method $this dontSeeLink([optional])
* @method $this seeCheckboxIsChecked([optional])
* @method $this dontSeeCheckboxIsChecked([optional])
* @method $this seeInField([optional])
* @method $this dontSeeInField([optional])
* @method $this seeInFormFields([optional])
* @method $this dontSeeInFormFields([optional])
* @method $this selectOption([optional])
* @method $this checkOption([optional])
* @method $this uncheckOption([optional])
* @method $this fillField([optional])
* @method $this attachFile([optional])
* @method $this seeNumberOfElements([optional])
* @method $this seeOptionIsSelected([optional])
* @method $this dontSeeOptionIsSelected([optional])
*/
class ActionSequence
{
protected $actions = [];
/**
* Creates an instance
* @return ActionSequence
*/
public static function build()
{
return new self;
}
public function __call($action, $arguments)
{
$this->addAction($action, $arguments);
return $this;
}
protected function addAction($action, $arguments)
{
if (!is_array($arguments)) {
$arguments = [$arguments];
}
$this->actions[] = new Action($action, $arguments);
}
/**
* Creates action sequence from associative array,
* where key is action, and value is action arguments
*
* @param array $actions
* @return $this
*/
public function fromArray(array $actions)
{
foreach ($actions as $action => $arguments) {
$this->addAction($action, $arguments);
}
return $this;
}
/**
* Returns a list of logged actions as associative array
* @return array
*/
public function toArray()
{
return $this->actions;
}
/**
* Executes sequence of action as methods of passed object.
*
* @param $context
*/
public function run($context)
{
foreach ($this->actions as $step) {
/** @var $step Action **/
codecept_debug("- $step");
try {
call_user_func_array([$context, $step->getAction()], $step->getArguments());
} catch (\Exception $e) {
$class = get_class($e); // rethrow exception for a specific action
throw new $class($e->getMessage() . "\nat $step");
}
}
}
public function __toString()
{
$actionsLog = [];
foreach ($this->actions as $step) {
$args = str_replace('"', "'", $step->getArgumentsAsString(20));
$actionsLog[] = $step->getAction() . ": $args";
}
return implode(', ', $actionsLog);
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Codeception\Util;
/**
* Simple annotation parser. Take only key-value annotations for methods or class.
*/
class Annotation
{
protected static $reflectedClasses = [];
protected static $regex = '/@%s(?:[ \t]*(.*?))?[ \t]*\r?$/m';
protected static $lastReflected = null;
/**
* @var \ReflectionClass
*/
protected $reflectedClass;
protected $currentReflectedItem;
/**
* Grabs annotation values.
*
* Usage example:
*
* ``` php
* <?php
* Annotation::forClass('MyTestCase')->fetch('guy');
* Annotation::forClass('MyTestCase')->method('testData')->fetch('depends');
* Annotation::forClass('MyTestCase')->method('testData')->fetchAll('depends');
*
* ?>
* ```
*
* @param $class
*
* @return $this
*/
public static function forClass($class)
{
if (is_object($class)) {
$class = get_class($class);
}
if (!isset(static::$reflectedClasses[$class])) {
static::$reflectedClasses[$class] = new \ReflectionClass($class);
}
return new static(static::$reflectedClasses[$class]);
}
/**
* @param $class
* @param $method
*
* @return $this
*/
public static function forMethod($class, $method)
{
return self::forClass($class)->method($method);
}
/**
* Parses raw comment for annotations
*
* @param $docblock
* @param $annotation
* @return array
*/
public static function fetchAnnotationsFromDocblock($annotation, $docblock)
{
if (preg_match_all(sprintf(self::$regex, $annotation), $docblock, $matched)) {
return $matched[1];
}
return [];
}
/**
* Fetches all available annotations
*
* @param $docblock
* @return array
*/
public static function fetchAllAnnotationsFromDocblock($docblock)
{
$annotations = [];
if (!preg_match_all(sprintf(self::$regex, '(\w+)'), $docblock, $matched)) {
return $annotations;
}
foreach ($matched[1] as $k => $annotation) {
if (!isset($annotations[$annotation])) {
$annotations[$annotation] = [];
}
$annotations[$annotation][] = $matched[2][$k];
};
return $annotations;
}
public function __construct(\ReflectionClass $class)
{
$this->currentReflectedItem = $this->reflectedClass = $class;
}
/**
* @param $method
*
* @return $this
*/
public function method($method)
{
$this->currentReflectedItem = $this->reflectedClass->getMethod($method);
return $this;
}
/**
* @param $annotation
* @return null
*/
public function fetch($annotation)
{
$docBlock = $this->currentReflectedItem->getDocComment();
if (preg_match(sprintf(self::$regex, $annotation), $docBlock, $matched)) {
return $matched[1];
}
return null;
}
/**
* @param $annotation
* @return array
*/
public function fetchAll($annotation)
{
$docBlock = $this->currentReflectedItem->getDocComment();
if (preg_match_all(sprintf(self::$regex, $annotation), $docBlock, $matched)) {
return $matched[1];
}
return [];
}
public function raw()
{
return $this->currentReflectedItem->getDocComment();
}
/**
* Returns an associative array value of annotation
* Either JSON or Doctrine-annotation style allowed
* Returns null if not a valid array data
*
* @param $annotation
* @return array|mixed|string
*/
public static function arrayValue($annotation)
{
$annotation = trim($annotation);
$openingBrace = substr($annotation, 0, 1);
// json-style data format
if (in_array($openingBrace, ['{', '['])) {
return json_decode($annotation, true);
}
// doctrine-style data format
if ($openingBrace === '(') {
preg_match_all('~(\w+)\s*?=\s*?"(.*?)"\s*?[,)]~', $annotation, $matches, PREG_SET_ORDER);
$data = [];
foreach ($matches as $item) {
$data[$item[1]] = $item[2];
}
return $data;
}
return null;
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace Codeception\Util;
class ArrayContainsComparator
{
/**
* @var array
*/
protected $haystack = [];
public function __construct($haystack)
{
$this->haystack = $haystack;
}
/**
* @return array
*/
public function getHaystack()
{
return $this->haystack;
}
public function containsArray(array $needle)
{
return $needle == $this->arrayIntersectRecursive($needle, $this->haystack);
}
/**
* @author nleippe@integr8ted.com
* @author tiger.seo@gmail.com
* @link http://www.php.net/manual/en/function.array-intersect-assoc.php#39822
*
* @param mixed $arr1
* @param mixed $arr2
*
* @return array|bool
*/
private function arrayIntersectRecursive($arr1, $arr2)
{
if (!is_array($arr1) || !is_array($arr2)) {
return false;
}
// if it is not an associative array we do not compare keys
if ($this->arrayIsSequential($arr1) && $this->arrayIsSequential($arr2)) {
return $this->sequentialArrayIntersect($arr1, $arr2);
}
return $this->associativeArrayIntersect($arr1, $arr2);
}
/**
* This array has sequential keys?
*
* @param array $array
*
* @return bool
*/
private function arrayIsSequential(array $array)
{
return array_keys($array) === range(0, count($array) - 1);
}
/**
* @param array $arr1
* @param array $arr2
* @return array
*/
private function sequentialArrayIntersect(array $arr1, array $arr2)
{
$ret = [];
// Do not match the same item of $arr2 against multiple items of $arr1
$matchedKeys = [];
foreach ($arr1 as $key1 => $value1) {
foreach ($arr2 as $key2 => $value2) {
if (isset($matchedKeys[$key2])) {
continue;
}
$return = $this->arrayIntersectRecursive($value1, $value2);
if ($return !== false && $return == $value1) {
$ret[$key1] = $return;
$matchedKeys[$key2] = true;
break;
}
if ($this->isEqualValue($value1, $value2)) {
$ret[$key1] = $value1;
$matchedKeys[$key2] = true;
break;
}
}
}
return $ret;
}
/**
* @param array $arr1
* @param array $arr2
*
* @return array|bool|null
*/
private function associativeArrayIntersect(array $arr1, array $arr2)
{
$commonKeys = array_intersect(array_keys($arr1), array_keys($arr2));
$ret = [];
foreach ($commonKeys as $key) {
$return = $this->arrayIntersectRecursive($arr1[$key], $arr2[$key]);
if ($return) {
$ret[$key] = $return;
continue;
}
if ($this->isEqualValue($arr1[$key], $arr2[$key])) {
$ret[$key] = $arr1[$key];
}
}
if (empty($commonKeys)) {
foreach ($arr2 as $arr) {
$return = $this->arrayIntersectRecursive($arr1, $arr);
if ($return && $return == $arr1) {
return $return;
}
}
}
if (count($ret) < min(count($arr1), count($arr2))) {
return null;
}
return $ret;
}
private function isEqualValue($val1, $val2)
{
if (is_numeric($val1)) {
$val1 = (string) $val1;
}
if (is_numeric($val2)) {
$val2 = (string) $val2;
}
return $val1 === $val2;
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Codeception\Util;
/**
* Autoloader, which is fully compatible with PSR-4,
* and can be used to autoload your `Helper`, `Page`, and `Step` classes.
*/
class Autoload
{
protected static $registered = false;
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
* @var array
*/
protected static $map = [];
private function __construct()
{
}
/**
* Adds a base directory for a namespace prefix.
*
* Example:
*
* ```php
* <?php
* // app\Codeception\UserHelper will be loaded from '/path/to/helpers/UserHelper.php'
* Autoload::addNamespace('app\Codeception', '/path/to/helpers');
*
* // LoginPage will be loaded from '/path/to/pageobjects/LoginPage.php'
* Autoload::addNamespace('', '/path/to/pageobjects');
*
* Autoload::addNamespace('app\Codeception', '/path/to/controllers');
* ?>
* ```
*
* @param string $prefix The namespace prefix.
* @param string $base_dir A base directory for class files in the namespace.
* @param bool $prepend If true, prepend the base directory to the stack instead of appending it;
* this causes it to be searched first rather than last.
* @return void
*/
public static function addNamespace($prefix, $base_dir, $prepend = false)
{
if (!self::$registered) {
spl_autoload_register([__CLASS__, 'load']);
self::$registered = true;
}
// normalize namespace prefix
$prefix = trim($prefix, '\\') . '\\';
// normalize the base directory with a trailing separator
$base_dir = rtrim($base_dir, '/') . DIRECTORY_SEPARATOR;
$base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
// initialize the namespace prefix array
if (isset(self::$map[$prefix]) === false) {
self::$map[$prefix] = [];
}
// retain the base directory for the namespace prefix
if ($prepend) {
array_unshift(self::$map[$prefix], $base_dir);
} else {
array_push(self::$map[$prefix], $base_dir);
}
}
/**
* @deprecated Use self::addNamespace() instead.
*/
public static function register($namespace, $suffix, $path)
{
self::addNamespace($namespace, $path);
}
/**
* @deprecated Use self::addNamespace() instead.
*/
public static function registerSuffix($suffix, $path)
{
self::addNamespace('', $path);
}
public static function load($class)
{
// the current namespace prefix
$prefix = $class;
// work backwards through the namespace names of the fully-qualified class name to find a mapped file name
while (false !== ($pos = strrpos($prefix, '\\'))) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
// the rest is the relative class name
$relative_class = substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mapped_file = self::loadMappedFile($prefix, $relative_class);
if ($mapped_file) {
return $mapped_file;
}
// remove the trailing namespace separator for the next iteration of strrpos()
$prefix = rtrim($prefix, '\\');
}
// fix for empty prefix
if (isset(self::$map['\\']) && ($class[0] != '\\')) {
return self::load('\\' . $class);
}
// backwards compatibility with old autoloader
// :TODO: it should be removed
if (strpos($class, '\\') !== false) {
$relative_class = substr(strrchr($class, '\\'), 1); // Foo\Bar\ClassName -> ClassName
$mapped_file = self::loadMappedFile('\\', $relative_class);
if ($mapped_file) {
return $mapped_file;
}
}
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relative_class The relative class name.
* @return mixed Boolean false if no mapped file can be loaded, or the name of the mapped file that was loaded.
*/
protected static function loadMappedFile($prefix, $relative_class)
{
if (!isset(self::$map[$prefix])) {
return false;
}
foreach (self::$map[$prefix] as $base_dir) {
$file = $base_dir
. str_replace('\\', '/', $relative_class)
. '.php';
// 'static' is for testing purposes
if (static::requireFile($file)) {
return $file;
}
}
return false;
}
protected static function requireFile($file)
{
if (file_exists($file)) {
require_once $file;
return true;
}
return false;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Codeception\Util;
use Codeception\Lib\Console\Output;
/**
* This class is used only when Codeception is executed in `--debug` mode.
* In other cases method of this class won't be seen.
*/
class Debug
{
/**
* @var Output null
*/
protected static $output = null;
public static function setOutput(Output $output)
{
self::$output = $output;
}
/**
* Prints data to screen. Message can be any time of data
*
* @param $message
*/
public static function debug($message)
{
if (!self::$output) {
return;
}
self::$output->debug($message);
}
/**
* Pauses execution and waits for user input to proceed.
*/
public static function pause()
{
if (!self::$output) {
return;
}
self::$output->writeln("<info>The execution has been paused. Press ENTER to continue</info>");
if (trim(fgets(STDIN)) != chr(13)) {
return;
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Codeception\Util;
/**
* Set of functions to work with Filesystem
*
*/
class FileSystem
{
/**
* @param $path
*/
public static function doEmptyDir($path)
{
/** @var $iterator \RecursiveIteratorIterator|\SplFileObject[] */
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $path) {
$basename = basename((string)$path);
if ($basename === '.' || $basename === '..' || $basename === '.gitignore') {
continue;
}
if ($path->isDir()) {
rmdir((string)$path);
} else {
unlink((string)$path);
}
}
}
/**
* @param $dir
* @return bool
*/
public static function deleteDir($dir)
{
if (!file_exists($dir)) {
return true;
}
if (!is_dir($dir) || is_link($dir)) {
return @unlink($dir);
}
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$dir = str_replace('/', '\\', $dir);
exec('rd /s /q "'.$dir.'"');
return true;
}
foreach (scandir($dir) as $item) {
if ($item === '.' || $item === '..') {
continue;
}
if (!self::deleteDir($dir . DIRECTORY_SEPARATOR . $item)) {
chmod($dir . DIRECTORY_SEPARATOR . $item, 0777);
if (!self::deleteDir($dir . DIRECTORY_SEPARATOR . $item)) {
return false;
}
}
}
return @rmdir($dir);
}
/**
* @param $src
* @param $dst
*/
public static function copyDir($src, $dst)
{
$dir = opendir($src);
@mkdir($dst);
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..')) {
if (is_dir($src . DIRECTORY_SEPARATOR . $file)) {
self::copyDir($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
} else {
copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
}
}
}
closedir($dir);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Codeception\Util;
/**
* Really basic class to store data in global array and use it in Cests/Tests.
*
* ```php
* <?php
* Fixtures::add('user1', ['name' => 'davert']);
* Fixtures::get('user1');
* Fixtures::exists('user1');
*
* ?>
* ```
*
*/
class Fixtures
{
protected static $fixtures = [];
public static function add($name, $data)
{
self::$fixtures[$name] = $data;
}
public static function get($name)
{
if (!self::exists($name)) {
throw new \RuntimeException("$name not found in fixtures");
}
return self::$fixtures[$name];
}
public static function cleanup()
{
self::$fixtures = [];
}
public static function exists($name)
{
return isset(self::$fixtures[$name]);
}
}

View File

@@ -0,0 +1,162 @@
<?php
namespace Codeception\Util;
/**
* Class containing constants of HTTP Status Codes
* and method to print HTTP code with its description.
*
* Usage:
*
* ```php
* <?php
* use \Codeception\Util\HttpCode;
*
* // using REST, PhpBrowser, or any Framework module
* $I->seeResponseCodeIs(HttpCode::OK);
* $I->dontSeeResponseCodeIs(HttpCode::NOT_FOUND);
* ```
*
*
*/
class HttpCode
{
const SWITCHING_PROTOCOLS = 101;
const PROCESSING = 102; // RFC2518
const OK = 200;
const CREATED = 201;
const ACCEPTED = 202;
const NON_AUTHORITATIVE_INFORMATION = 203;
const NO_CONTENT = 204;
const RESET_CONTENT = 205;
const PARTIAL_CONTENT = 206;
const MULTI_STATUS = 207; // RFC4918
const ALREADY_REPORTED = 208; // RFC5842
const IM_USED = 226; // RFC3229
const MULTIPLE_CHOICES = 300;
const MOVED_PERMANENTLY = 301;
const FOUND = 302;
const SEE_OTHER = 303;
const NOT_MODIFIED = 304;
const USE_PROXY = 305;
const RESERVED = 306;
const TEMPORARY_REDIRECT = 307;
const PERMANENTLY_REDIRECT = 308; // RFC7238
const BAD_REQUEST = 400;
const UNAUTHORIZED = 401;
const PAYMENT_REQUIRED = 402;
const FORBIDDEN = 403;
const NOT_FOUND = 404;
const METHOD_NOT_ALLOWED = 405;
const NOT_ACCEPTABLE = 406;
const PROXY_AUTHENTICATION_REQUIRED = 407;
const REQUEST_TIMEOUT = 408;
const CONFLICT = 409;
const GONE = 410;
const LENGTH_REQUIRED = 411;
const PRECONDITION_FAILED = 412;
const REQUEST_ENTITY_TOO_LARGE = 413;
const REQUEST_URI_TOO_LONG = 414;
const UNSUPPORTED_MEDIA_TYPE = 415;
const REQUESTED_RANGE_NOT_SATISFIABLE = 416;
const EXPECTATION_FAILED = 417;
const I_AM_A_TEAPOT = 418; // RFC2324
const UNPROCESSABLE_ENTITY = 422; // RFC4918
const LOCKED = 423; // RFC4918
const FAILED_DEPENDENCY = 424; // RFC4918
const RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL = 425; // RFC2817
const UPGRADE_REQUIRED = 426; // RFC2817
const PRECONDITION_REQUIRED = 428; // RFC6585
const TOO_MANY_REQUESTS = 429; // RFC6585
const REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
const INTERNAL_SERVER_ERROR = 500;
const NOT_IMPLEMENTED = 501;
const BAD_GATEWAY = 502;
const SERVICE_UNAVAILABLE = 503;
const GATEWAY_TIMEOUT = 504;
const VERSION_NOT_SUPPORTED = 505;
const VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
const INSUFFICIENT_STORAGE = 507; // RFC4918
const LOOP_DETECTED = 508; // RFC5842
const NOT_EXTENDED = 510; // RFC2774
const NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
private static $codes = [
100 => 'Continue',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required'
];
/**
* Returns string with HTTP code and its description
*
* ```php
* <?php
* HttpCode::getDescription(200); // '200 (OK)'
* HttpCode::getDescription(401); // '401 (Unauthorized)'
* ```
*
* @param $code
* @return mixed
*/
public static function getDescription($code)
{
if (isset(self::$codes[$code])) {
return sprintf('%d (%s)', $code, self::$codes[$code]);
}
return $code;
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Codeception\Util;
use Flow\JSONPath\JSONPath;
use InvalidArgumentException;
use DOMDocument;
class JsonArray
{
/**
* @var array
*/
protected $jsonArray = [];
/**
* @var DOMDocument
*/
protected $jsonXml = null;
public function __construct($jsonString)
{
if (!is_string($jsonString)) {
throw new InvalidArgumentException('$jsonString param must be a string.');
}
$jsonDecode = json_decode($jsonString, true);
if (!is_array($jsonDecode)) {
$jsonDecode = [$jsonDecode];
}
$this->jsonArray = $jsonDecode;
if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidArgumentException(
sprintf(
"Invalid json: %s. System message: %s.",
$jsonString,
json_last_error_msg()
),
json_last_error()
);
}
}
public function toXml()
{
if ($this->jsonXml) {
return $this->jsonXml;
}
$root = 'root';
$jsonArray = $this->jsonArray;
if (count($jsonArray) == 1) {
$value = reset($jsonArray);
if (is_array($value)) {
$root = key($jsonArray);
$jsonArray = $value;
}
}
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = false;
$root = $dom->createElement($root);
$dom->appendChild($root);
$this->arrayToXml($dom, $root, $jsonArray);
$this->jsonXml = $dom;
return $dom;
}
/**
* @return array
*/
public function toArray()
{
return $this->jsonArray;
}
public function filterByXPath($xpath)
{
$path = new \DOMXPath($this->toXml());
return $path->query($xpath);
}
public function filterByJsonPath($jsonPath)
{
if (!class_exists('Flow\JSONPath\JSONPath')) {
throw new \Exception('JSONPath library not installed. Please add `flow/jsonpath` to composer.json');
}
return (new JSONPath($this->jsonArray))->find($jsonPath)->data();
}
public function getXmlString()
{
return $this->toXml()->saveXML();
}
public function containsArray(array $needle)
{
return (new ArrayContainsComparator($this->jsonArray))->containsArray($needle);
}
private function arrayToXml(\DOMDocument $doc, \DOMNode $node, $array)
{
foreach ($array as $key => $value) {
if (is_numeric($key)) {
$subNode = $doc->createElement($node->nodeName);
$node->parentNode->appendChild($subNode);
} else {
try {
$subNode = $doc->createElement($key);
} catch (\Exception $e) {
$key = $this->getValidTagNameForInvalidKey($key);
$subNode = $doc->createElement($key);
}
$node->appendChild($subNode);
}
if (is_array($value)) {
$this->arrayToXml($doc, $subNode, $value);
} else {
$subNode->nodeValue = htmlspecialchars((string)$value);
}
}
}
private function getValidTagNameForInvalidKey($key)
{
static $map = [];
if (!isset($map[$key])) {
$tagName = 'invalidTag' . (count($map) + 1);
$map[$key] = $tagName;
codecept_debug($tagName . ' is "' . $key . '"');
}
return $map[$key];
}
}

View File

@@ -0,0 +1,208 @@
<?php
namespace Codeception\Util;
/**
* JsonType matches JSON structures against templates.
* You can specify the type of fields in JSON or add additional validation rules.
*
* JsonType is used by REST module in `seeResponseMatchesJsonType` and `dontSeeResponseMatchesJsonType` methods.
*
* Usage example:
*
* ```php
* <?php
* $jsonType = new JsonType(['name' => 'davert', 'id' => 1]);
* $jsonType->matches([
* 'name' => 'string:!empty',
* 'id' => 'integer:>0|string:>0',
* ]); // => true
*
* $jsonType->matches([
* 'id' => 'string',
* ]); // => `id: 1` is not of type string
* ?>
* ```
*
* Class JsonType
* @package Codeception\Util
*/
class JsonType
{
protected $jsonArray;
protected static $customFilters = [];
/**
* Creates instance of JsonType
* Pass an array or `\Codeception\Util\JsonArray` with data.
* If non-associative array is passed - the very first element of it will be used for matching.
*
* @param $jsonArray array|\Codeception\Util\JsonArray
*/
public function __construct($jsonArray)
{
if ($jsonArray instanceof JsonArray) {
$jsonArray = $jsonArray->toArray();
}
$this->jsonArray = $jsonArray;
}
/**
* Adds custom filter to JsonType list.
* You should specify a name and parameters of a filter.
*
* Example:
*
* ```php
* <?php
* JsonType::addCustomFilter('slug', function($value) {
* return strpos(' ', $value) !== false;
* });
* // => use it as 'string:slug'
*
* // add custom function to matcher with `len($val)` syntax
* // parameter matching patterns should be valid regex and start with `/` char
* JsonType::addCustomFilter('/len\((.*?)\)/', function($value, $len) {
* return strlen($value) == $len;
* });
* // use it as 'string:len(5)'
* ?>
* ```
*
* @param $name
* @param callable $callable
*/
public static function addCustomFilter($name, callable $callable)
{
static::$customFilters[$name] = $callable;
}
/**
* Removes all custom filters
*/
public static function cleanCustomFilters()
{
static::$customFilters = [];
}
/**
* Checks data against passed JsonType.
* If matching fails function returns a string with a message describing failure.
* On success returns `true`.
*
* @param array $jsonType
* @return bool|string
*/
public function matches(array $jsonType)
{
if (array_key_exists(0, $this->jsonArray) && is_array($this->jsonArray[0])) {
// a list of items
$msg = '';
foreach ($this->jsonArray as $array) {
$res = $this->typeComparison($array, $jsonType);
if ($res !== true) {
$msg .= "\n" . $res;
}
}
if ($msg) {
return $msg;
}
return true;
}
return $this->typeComparison($this->jsonArray, $jsonType);
}
protected function typeComparison($data, $jsonType)
{
foreach ($jsonType as $key => $type) {
if (!array_key_exists($key, $data)) {
return "Key `$key` doesn't exist in " . json_encode($data);
}
if (is_array($jsonType[$key])) {
$message = $this->typeComparison($data[$key], $jsonType[$key]);
if (is_string($message)) {
return $message;
}
continue;
}
$matchTypes = preg_split("#(?![^]\(]*\))\|#", $type);
$matched = false;
$currentType = strtolower(gettype($data[$key]));
if ($currentType == 'double') {
$currentType = 'float';
}
foreach ($matchTypes as $matchType) {
$filters = preg_split("#(?![^]\(]*\))\:#", $matchType);
$expectedType = trim(strtolower(array_shift($filters)));
if ($expectedType != $currentType) {
continue;
}
$matched = true;
foreach ($filters as $filter) {
$matched = $matched && $this->matchFilter($filter, $data[$key]);
}
if ($matched) {
break;
}
}
if (!$matched) {
return sprintf("`$key: %s` is of type `$type`", var_export($data[$key], true));
}
}
return true;
}
protected function matchFilter($filter, $value)
{
$filter = trim($filter);
if (strpos($filter, '!') === 0) {
return !$this->matchFilter(substr($filter, 1), $value);
}
// apply custom filters
foreach (static::$customFilters as $customFilter => $callable) {
if (strpos($customFilter, '/') === 0) {
if (preg_match($customFilter, $filter, $matches)) {
array_shift($matches);
return call_user_func_array($callable, array_merge([$value], $matches));
}
}
if ($customFilter == $filter) {
return $callable($value);
}
}
if (strpos($filter, '=') === 0) {
return $value == substr($filter, 1);
}
if ($filter === 'url') {
return filter_var($value, FILTER_VALIDATE_URL);
}
if ($filter === 'date') {
return preg_match(
'/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(?:Z|(\+|-)([\d|:]*))?$/',
$value
);
}
if ($filter === 'email') { // from http://emailregex.com/
// @codingStandardsIgnoreStart
return preg_match('/^(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){255,})(?!(?:(?:\x22?\x5C[\x00-\x7E]\x22?)|(?:\x22?[^\x5C\x22]\x22?)){65,}@)(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22))(?:\.(?:(?:[\x21\x23-\x27\x2A\x2B\x2D\x2F-\x39\x3D\x3F\x5E-\x7E]+)|(?:\x22(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7F]|(?:\x5C[\x00-\x7F]))*\x22)))*@(?:(?:(?!.*[^.]{64,})(?:(?:(?:xn--)?[a-z0-9]+(?:-[a-z0-9]+)*\.){1,126}){1,}(?:(?:[a-z][a-z0-9]*)|(?:(?:xn--)[a-z0-9]+))(?:-[a-z0-9]+)*)|(?:\[(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){7})|(?:(?!(?:.*[a-f0-9][:\]]){7,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,5})?)))|(?:(?:IPv6:(?:(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){5}:)|(?:(?!(?:.*[a-f0-9]:){5,})(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3})?::(?:[a-f0-9]{1,4}(?::[a-f0-9]{1,4}){0,3}:)?)))?(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))(?:\.(?:(?:25[0-5])|(?:2[0-4][0-9])|(?:1[0-9]{2})|(?:[1-9]?[0-9]))){3}))\]))$/iD', $value);
// @codingStandardsIgnoreEnd
}
if ($filter === 'empty') {
return empty($value);
}
if (preg_match('~^regex\((.*?)\)$~', $filter, $matches)) {
return preg_match($matches[1], $value);
}
if (preg_match('~^>([\d\.]+)$~', $filter, $matches)) {
return (float)$value > (float)$matches[1];
}
if (preg_match('~^<([\d\.]+)$~', $filter, $matches)) {
return (float)$value < (float)$matches[1];
}
}
}

View File

@@ -0,0 +1,400 @@
<?php
namespace Codeception\Util;
use Facebook\WebDriver\WebDriverBy;
use Symfony\Component\CssSelector\CssSelectorConverter;
use Symfony\Component\CssSelector\Exception\ParseException;
use Symfony\Component\CssSelector\XPath\Translator;
/**
* Set of useful functions for using CSS and XPath locators.
* Please check them before writing complex functional or acceptance tests.
*
*/
class Locator
{
/**
* Applies OR operator to any number of CSS or XPath selectors.
* You can mix up CSS and XPath selectors here.
*
* ```php
* <?php
* use \Codeception\Util\Locator;
*
* $I->see('Title', Locator::combine('h1','h2','h3'));
* ?>
* ```
*
* This will search for `Title` text in either `h1`, `h2`, or `h3` tag.
* You can also combine CSS selector with XPath locator:
*
* ```php
* <?php
* use \Codeception\Util\Locator;
*
* $I->fillField(Locator::combine('form input[type=text]','//form/textarea[2]'), 'qwerty');
* ?>
* ```
*
* As a result the Locator will produce a mixed XPath value that will be used in fillField action.
*
* @static
*
* @param $selector1
* @param $selector2
*
* @throws \Exception
*
* @return string
*/
public static function combine($selector1, $selector2)
{
$selectors = func_get_args();
foreach ($selectors as $k => $v) {
$selectors[$k] = self::toXPath($v);
if (!$selectors[$k]) {
throw new \Exception("$v is invalid CSS or XPath");
}
}
return implode(' | ', $selectors);
}
/**
* Matches the *a* element with given URL
*
* ```php
* <?php
* use \Codeception\Util\Locator;
*
* $I->see('Log In', Locator::href('/login.php'));
* ?>
* ```
*
* @static
*
* @param $url
*
* @return string
*/
public static function href($url)
{
return sprintf('//a[@href=normalize-space(%s)]', Translator::getXpathLiteral($url));
}
/**
* Matches the element with given tab index
*
* Do you often use the `TAB` key to navigate through the web page? How do your site respond to this navigation?
* You could try to match elements by their tab position using `tabIndex` method of `Locator` class.
* ```php
* <?php
* use \Codeception\Util\Locator;
*
* $I->fillField(Locator::tabIndex(1), 'davert');
* $I->fillField(Locator::tabIndex(2) , 'qwerty');
* $I->click('Login');
* ?>
* ```
*
* @static
*
* @param $index
*
* @return string
*/
public static function tabIndex($index)
{
return sprintf('//*[@tabindex = normalize-space(%d)]', $index);
}
/**
* Matches option by text:
*
* ```php
* <?php
* use Codeception\Util\Locator;
*
* $I->seeElement(Locator::option('Male'), '#select-gender');
* ```
*
* @param $value
*
* @return string
*/
public static function option($value)
{
return sprintf('//option[.=normalize-space("%s")]', $value);
}
protected static function toXPath($selector)
{
try {
$xpath = (new CssSelectorConverter())->toXPath($selector);
return $xpath;
} catch (ParseException $e) {
if (self::isXPath($selector)) {
return $selector;
}
}
return null;
}
/**
* Finds element by it's attribute(s)
*
* ```php
* <?php
* use \Codeception\Util\Locator;
*
* $I->seeElement(Locator::find('img', ['title' => 'diagram']));
* ```
*
* @static
*
* @param $element
* @param $attributes
*
* @return string
*/
public static function find($element, array $attributes)
{
$operands = [];
foreach ($attributes as $attribute => $value) {
if (is_int($attribute)) {
$operands[] = '@' . $value;
} else {
$operands[] = '@' . $attribute . ' = ' . Translator::getXpathLiteral($value);
}
}
return sprintf('//%s[%s]', $element, implode(' and ', $operands));
}
/**
* Checks that provided string is CSS selector
*
* ```php
* <?php
* Locator::isCSS('#user .hello') => true
* Locator::isCSS('body') => true
* Locator::isCSS('//body/p/user') => false
* ```
*
* @param $selector
*
* @return bool
*/
public static function isCSS($selector)
{
try {
(new CssSelectorConverter())->toXPath($selector);
} catch (ParseException $e) {
return false;
}
return true;
}
/**
* Checks that locator is an XPath
*
* ```php
* <?php
* Locator::isXPath('#user .hello') => false
* Locator::isXPath('body') => false
* Locator::isXPath('//body/p/user') => true
* ```
*
* @param $locator
*
* @return bool
*/
public static function isXPath($locator)
{
$document = new \DOMDocument('1.0', 'UTF-8');
$xpath = new \DOMXPath($document);
return @$xpath->evaluate($locator, $document) !== false;
}
/**
* @param $locator
* @return bool
*/
public static function isPrecise($locator)
{
if (is_array($locator)) {
return true;
}
if ($locator instanceof WebDriverBy) {
return true;
}
if (Locator::isID($locator)) {
return true;
}
if (strpos($locator, '//') === 0) {
return true; // simple xpath check
}
return false;
}
/**
* Checks that a string is valid CSS ID
*
* ```php
* <?php
* Locator::isID('#user') => true
* Locator::isID('body') => false
* Locator::isID('//body/p/user') => false
* ```
*
* @param $id
*
* @return bool
*/
public static function isID($id)
{
return (bool)preg_match('~^#[\w\.\-\[\]\=\^\~\:]+$~', $id);
}
/**
* Checks that a string is valid CSS class
*
* ```php
* <?php
* Locator::isClass('.hello') => true
* Locator::isClass('body') => false
* Locator::isClass('//body/p/user') => false
* ```
*
* @param $class
* @return bool
*/
public static function isClass($class)
{
return (bool)preg_match('~^\.[\w\.\-\[\]\=\^\~\:]+$~', $class);
}
/**
* Locates an element containing a text inside.
* Either CSS or XPath locator can be passed, however they will be converted to XPath.
*
* ```php
* <?php
* use Codeception\Util\Locator;
*
* Locator::contains('label', 'Name'); // label containing name
* Locator::contains('div[@contenteditable=true]', 'hello world');
* ```
*
* @param $element
* @param $text
*
* @return string
*/
public static function contains($element, $text)
{
$text = Translator::getXpathLiteral($text);
return sprintf('%s[%s]', self::toXPath($element), "contains(., $text)");
}
/**
* Locates element at position.
* Either CSS or XPath locator can be passed as locator,
* position is an integer. If a negative value is provided, counting starts from the last element.
* First element has index 1
*
* ```php
* <?php
* use Codeception\Util\Locator;
*
* Locator::elementAt('//table/tr', 2); // second row
* Locator::elementAt('//table/tr', -1); // last row
* Locator::elementAt('table#grind>tr', -2); // previous than last row
* ```
*
* @param string $element CSS or XPath locator
* @param int $position xpath index
*
* @return mixed
*/
public static function elementAt($element, $position)
{
if (is_int($position) && $position < 0) {
$position++; // -1 points to the last element
$position = 'last()-'.abs($position);
}
if ($position === 0) {
throw new \InvalidArgumentException(
'0 is not valid element position. XPath expects first element to have index 1'
);
}
return sprintf('(%s)[position()=%s]', self::toXPath($element), $position);
}
/**
* Locates first element of group elements.
* Either CSS or XPath locator can be passed as locator,
* Equal to `Locator::elementAt($locator, 1)`
*
* ```php
* <?php
* use Codeception\Util\Locator;
*
* Locator::firstElement('//table/tr');
* ```
*
* @param $element
*
* @return mixed
*/
public static function firstElement($element)
{
return self::elementAt($element, 1);
}
/**
* Locates last element of group elements.
* Either CSS or XPath locator can be passed as locator,
* Equal to `Locator::elementAt($locator, -1)`
*
* ```php
* <?php
* use Codeception\Util\Locator;
*
* Locator::lastElement('//table/tr');
* ```
*
* @param $element
*
* @return mixed
*/
public static function lastElement($element)
{
return self::elementAt($element, 'last()');
}
/**
* Transforms strict locator, \Facebook\WebDriver\WebDriverBy into a string represenation
*
* @param $selector
*
* @return string
*/
public static function humanReadableString($selector)
{
if (is_string($selector)) {
return "'$selector'";
}
if (is_array($selector)) {
$type = strtolower(key($selector));
$locator = $selector[$type];
return "$type '$locator'";
}
if (class_exists('\Facebook\WebDriver\WebDriverBy')) {
if ($selector instanceof WebDriverBy) {
$type = $selector->getMechanism();
$locator = $selector->getValue();
return "$type '$locator'";
}
}
throw new \InvalidArgumentException("Unrecognized selector");
}
}

View File

@@ -0,0 +1,245 @@
<?php
namespace Codeception\Util;
/**
* Class to represent any type of content.
* This class can act as an object, array, or string.
* Method or property calls to this class won't cause any errors.
*
* Maybe was used in Codeception 1.x to represent data on parsing step.
* Not widely used in 2.0 anymore, but left for compatibility.
*
* For instance, you may use `Codeception\Util\Maybe` as a test dummies.
*
* ```php
* <?php
* $user = new Maybe;
* $user->posts->comments->count();
* ?>
* ```
*/
class Maybe implements \ArrayAccess, \Iterator, \JsonSerializable
{
protected $position = 0;
protected $val = null;
protected $assocArray = null;
public function __construct($val = null)
{
$this->val = $val;
if (is_array($this->val)) {
$this->assocArray = $this->isAssocArray($this->val);
}
$this->position = 0;
}
private function isAssocArray($arr)
{
return array_keys($arr) !== range(0, count($arr) - 1);
}
public function __toString()
{
if ($this->val === null) {
return "?";
}
if (is_scalar($this->val)) {
return (string)$this->val;
}
if (is_object($this->val) && method_exists($this->val, '__toString')) {
return $this->val->__toString();
}
return $this->val;
}
public function __get($key)
{
if ($this->val === null) {
return new Maybe();
}
if (is_object($this->val)) {
if (isset($this->val->{$key}) || property_exists($this->val, $key)) {
return $this->val->{$key};
}
}
return $this->val->key;
}
public function __set($key, $val)
{
if ($this->val === null) {
return;
}
if (is_object($this->val)) {
$this->val->{$key} = $val;
return;
}
$this->val->key = $val;
}
public function __call($method, $args)
{
if ($this->val === null) {
return new Maybe();
}
return call_user_func_array([$this->val, $method], $args);
}
public function __clone()
{
if (is_object($this->val)) {
$this->val = clone $this->val;
}
}
public function __unset($key)
{
if (is_object($this->val)) {
if (isset($this->val->{$key}) || property_exists($this->val, $key)) {
unset($this->val->{$key});
return;
}
}
}
public function offsetExists($offset)
{
if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) {
return isset($this->val[$offset]);
}
return false;
}
public function offsetGet($offset)
{
if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) {
return $this->val[$offset];
}
return new Maybe();
}
public function offsetSet($offset, $value)
{
if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) {
$this->val[$offset] = $value;
}
}
public function offsetUnset($offset)
{
if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) {
unset($this->val[$offset]);
}
}
public function __value()
{
$val = $this->val;
if (is_array($val)) {
foreach ($val as $k => $v) {
if ($v instanceof self) {
$v = $v->__value();
}
$val[$k] = $v;
}
}
return $val;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
* @return mixed Can return any type.
*/
public function current()
{
if (!is_array($this->val)) {
return null;
}
if ($this->assocArray) {
$keys = array_keys($this->val);
return $this->val[$keys[$this->position]];
}
return $this->val[$this->position];
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
* @return void Any returned value is ignored.
*/
public function next()
{
++$this->position;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
if ($this->assocArray) {
$keys = array_keys($this->val);
return $keys[$this->position];
}
return $this->position;
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @return boolean The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
*/
public function valid()
{
if (!is_array($this->val)) {
return null;
}
if ($this->assocArray) {
$keys = array_keys($this->val);
return isset($keys[$this->position]);
}
return isset($this->val[$this->position]);
}
/**
* (PHP 5 &gt;= 5.0.0)<br/>
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @return void Any returned value is ignored.
*/
public function rewind()
{
if (is_array($this->val)) {
$this->assocArray = $this->isAssocArray($this->val);
}
$this->position = 0;
}
/**
* (PHP 5 >= 5.4.0)
* Serializes the object to a value that can be serialized natively by json_encode().
* @link http://docs.php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed Returns data which can be serialized by json_encode(), which is a value of any type other than a resource.
*/
public function jsonSerialize()
{
return $this->__value();
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Codeception\Util;
use Codeception\Exception\ConfigurationException;
class PathResolver
{
/**
* Returns path to a given directory relative to $projDir.
* @param string $path
* @param string $projDir
* @param string $dirSep
* @return string
*/
public static function getRelativeDir($path, $projDir, $dirSep = DIRECTORY_SEPARATOR)
{
// ensure $projDir ends with a trailing $dirSep
$projDir = preg_replace('/'.preg_quote($dirSep, '/').'*$/', $dirSep, $projDir);
// if $path is a within $projDir
if (self::fsCaseStrCmp(substr($path, 0, strlen($projDir)), $projDir, $dirSep) == 0) {
// simply chop it off the front
return substr($path, strlen($projDir));
}
// Identify any absoluteness prefix (like '/' in Unix or "C:\\" in Windows)
$pathAbsPrefix = self::getPathAbsolutenessPrefix($path, $dirSep);
$projDirAbsPrefix = self::getPathAbsolutenessPrefix($projDir, $dirSep);
$sameAbsoluteness = (self::fsCaseStrCmp($pathAbsPrefix['wholePrefix'], $projDirAbsPrefix['wholePrefix'], $dirSep) == 0);
if (!$sameAbsoluteness) {
// if the $projDir and $path aren't relative to the same
// thing, we can't make a relative path.
// if we're relative to the same device ...
if (strlen($pathAbsPrefix['devicePrefix']) &&
(self::fsCaseStrCmp($pathAbsPrefix['devicePrefix'], $projDirAbsPrefix['devicePrefix'], $dirSep) == 0)
) {
// ... shave that off
return substr($path, strlen($pathAbsPrefix['devicePrefix']));
}
// Return the input unaltered
return $path;
}
// peel off optional absoluteness prefixes and convert
// $path and $projDir to an subdirectory path array
$relPathParts = array_filter(explode($dirSep, substr($path, strlen($pathAbsPrefix['wholePrefix']))), 'strlen');
$relProjDirParts = array_filter(explode($dirSep, substr($projDir, strlen($projDirAbsPrefix['wholePrefix']))), 'strlen');
// While there are any, peel off any common parent directories
// from the beginning of the $projDir and $path
while ((count($relPathParts) > 0) && (count($relProjDirParts) > 0) &&
(self::fsCaseStrCmp($relPathParts[0], $relProjDirParts[0], $dirSep) == 0)
) {
array_shift($relPathParts);
array_shift($relProjDirParts);
}
if (count($relProjDirParts) > 0) {
// prefix $relPath with '..' for all remaining unmatched $projDir
// subdirectories
$relPathParts = array_merge(array_fill(0, count($relProjDirParts), '..'), $relPathParts);
}
// only append a trailing seperator if one is already present
$trailingSep = preg_match('/'.preg_quote($dirSep, '/').'$/', $path) ? $dirSep : '';
// convert array of dir paths back into a string path
return implode($dirSep, $relPathParts).$trailingSep;
}
/**
* FileSystem Case String Compare
* compare two strings with the filesystem's case-sensitiveness
*
* @param string $str1
* @param string $str2
* @param string $dirSep
* @return int -1 / 0 / 1 for < / = / > respectively
*/
private static function fsCaseStrCmp($str1, $str2, $dirSep = DIRECTORY_SEPARATOR)
{
$cmpFn = self::isWindows($dirSep) ? 'strcasecmp' : 'strcmp';
return $cmpFn($str1, $str2);
}
/**
* What part of this path (leftmost 0-3 characters) what
* it is absolute relative to:
*
* On Unix:
* This is simply '/' for an absolute path or
* '' for a relative path
*
* On Windows this is more complicated:
* If the first two characters are a letter followed
* by a ':', this indicates that the path is
* on a specific device.
* With or without a device specified, a path MAY
* start with a '\\' to indicate an absolute path
* on the device or '' to indicate a path relative
* to the device's CWD
*
* @param string $path
* @param string $dirSep
* @return string
*/
private static function getPathAbsolutenessPrefix($path, $dirSep = DIRECTORY_SEPARATOR)
{
$devLetterPrefixPattern = '';
if (self::isWindows($dirSep)) {
$devLetterPrefixPattern = '([A-Za-z]:|)';
}
$matches = [];
if (!preg_match('/^'.$devLetterPrefixPattern.preg_quote($dirSep, '/').'?/', $path, $matches)) {
// This should match, even if it matches 0 characters
throw new ConfigurationException("INTERNAL ERROR: This must be a regex problem.");
}
return [
'wholePrefix' => $matches[0], // The optional device letter followed by the optional $dirSep
'devicePrefix' => self::isWindows($dirSep) ? $matches[1] : ''];
}
/**
* Are we in a Windows style filesystem?
*
* @param string $dirSep
* @return bool
*/
private static function isWindows($dirSep = DIRECTORY_SEPARATOR)
{
return ($dirSep == '\\');
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Codeception\Util;
class PropertyAccess
{
/**
* @deprecated Use ReflectionHelper::readPrivateProperty()
* @param object $object
* @param string $property
* @param string|null $class
* @return mixed
*/
public static function readPrivateProperty($object, $property, $class = null)
{
return ReflectionHelper::readPrivateProperty($object, $property, $class);
}
}

View File

@@ -0,0 +1,3 @@
# Util Classes
Codeception Util classes are used by various parts of framework and can be used in tests or in other projects. They do not depend on Codeception core, nor they do know of Codeception itself. Thus, some of those classes have static methods.

View File

@@ -0,0 +1,63 @@
<?php
namespace Codeception\Util;
/**
* This class contains helper methods to help with common Reflection tasks.
*/
class ReflectionHelper
{
/**
* Read a private property of an object.
*
* @param object $object
* @param string $property
* @param string|null $class
* @return mixed
*/
public static function readPrivateProperty($object, $property, $class = null)
{
if (is_null($class)) {
$class = $object;
}
$property = new \ReflectionProperty($class, $property);
$property->setAccessible(true);
return $property->getValue($object);
}
/**
* Invoke a private method of an object.
*
* @param object $object
* @param string $method
* @param array $args
* @param string|null $class
* @return mixed
*/
public static function invokePrivateMethod($object, $method, $args = [], $class = null)
{
if (is_null($class)) {
$class = $object;
}
$method = new \ReflectionMethod($class, $method);
$method->setAccessible(true);
return $method->invokeArgs($object, $args);
}
/**
* Returns class name without namespace
*
* (does not use reflection actually)
*
* @param $object
* @return mixed
*/
public static function getClassShortName($object)
{
$path = explode('\\', get_class($object));
return array_pop($path);
}
}

View File

@@ -0,0 +1,462 @@
<?php
namespace Codeception\Util\Shared;
trait Asserts
{
protected function assert($arguments, $not = false)
{
$not = $not ? 'Not' : '';
$method = ucfirst(array_shift($arguments));
if (($method === 'True') && $not) {
$method = 'False';
$not = '';
}
if (($method === 'False') && $not) {
$method = 'True';
$not = '';
}
call_user_func_array(['\PHPUnit\Framework\Assert', 'assert' . $not . $method], $arguments);
}
protected function assertNot($arguments)
{
$this->assert($arguments, true);
}
/**
* Checks that two variables are equal.
*
* @param $expected
* @param $actual
* @param string $message
* @param float $delta
*/
protected function assertEquals($expected, $actual, $message = '', $delta = 0.0)
{
\PHPUnit\Framework\Assert::assertEquals($expected, $actual, $message, $delta);
}
/**
* Checks that two variables are not equal
*
* @param $expected
* @param $actual
* @param string $message
* @param float $delta
*/
protected function assertNotEquals($expected, $actual, $message = '', $delta = 0.0)
{
\PHPUnit\Framework\Assert::assertNotEquals($expected, $actual, $message, $delta);
}
/**
* Checks that two variables are same
*
* @param $expected
* @param $actual
* @param string $message
*/
protected function assertSame($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertSame($expected, $actual, $message);
}
/**
* Checks that two variables are not same
*
* @param $expected
* @param $actual
* @param string $message
*/
protected function assertNotSame($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertNotSame($expected, $actual, $message);
}
/**
* Checks that actual is greater than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
protected function assertGreaterThan($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertGreaterThan($expected, $actual, $message);
}
/**
* @deprecated
*/
protected function assertGreaterThen($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertGreaterThan($expected, $actual, $message);
}
/**
* Checks that actual is greater or equal than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
protected function assertGreaterThanOrEqual($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertGreaterThanOrEqual($expected, $actual, $message);
}
/**
* @deprecated
*/
protected function assertGreaterThenOrEqual($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertGreaterThanOrEqual($expected, $actual, $message);
}
/**
* Checks that actual is less than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
protected function assertLessThan($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertLessThan($expected, $actual, $message);
}
/**
* Checks that actual is less or equal than expected
*
* @param $expected
* @param $actual
* @param string $message
*/
protected function assertLessThanOrEqual($expected, $actual, $message = '')
{
\PHPUnit\Framework\Assert::assertLessThanOrEqual($expected, $actual, $message);
}
/**
* Checks that haystack contains needle
*
* @param $needle
* @param $haystack
* @param string $message
*/
protected function assertContains($needle, $haystack, $message = '')
{
\PHPUnit\Framework\Assert::assertContains($needle, $haystack, $message);
}
/**
* Checks that haystack doesn't contain needle.
*
* @param $needle
* @param $haystack
* @param string $message
*/
protected function assertNotContains($needle, $haystack, $message = '')
{
\PHPUnit\Framework\Assert::assertNotContains($needle, $haystack, $message);
}
/**
* Checks that string match with pattern
*
* @param string $pattern
* @param string $string
* @param string $message
*/
protected function assertRegExp($pattern, $string, $message = '')
{
\PHPUnit\Framework\Assert::assertRegExp($pattern, $string, $message);
}
/**
* Checks that string not match with pattern
*
* @param string $pattern
* @param string $string
* @param string $message
*/
protected function assertNotRegExp($pattern, $string, $message = '')
{
\PHPUnit\Framework\Assert::assertNotRegExp($pattern, $string, $message);
}
/**
* Checks that a string starts with the given prefix.
*
* @param string $prefix
* @param string $string
* @param string $message
*/
protected function assertStringStartsWith($prefix, $string, $message = '')
{
\PHPUnit\Framework\Assert::assertStringStartsWith($prefix, $string, $message);
}
/**
* Checks that a string doesn't start with the given prefix.
*
* @param string $prefix
* @param string $string
* @param string $message
*/
protected function assertStringStartsNotWith($prefix, $string, $message = '')
{
\PHPUnit\Framework\Assert::assertStringStartsNotWith($prefix, $string, $message);
}
/**
* Checks that variable is empty.
*
* @param $actual
* @param string $message
*/
protected function assertEmpty($actual, $message = '')
{
\PHPUnit\Framework\Assert::assertEmpty($actual, $message);
}
/**
* Checks that variable is not empty.
*
* @param $actual
* @param string $message
*/
protected function assertNotEmpty($actual, $message = '')
{
\PHPUnit\Framework\Assert::assertNotEmpty($actual, $message);
}
/**
* Checks that variable is NULL
*
* @param $actual
* @param string $message
*/
protected function assertNull($actual, $message = '')
{
\PHPUnit\Framework\Assert::assertNull($actual, $message);
}
/**
* Checks that variable is not NULL
*
* @param $actual
* @param string $message
*/
protected function assertNotNull($actual, $message = '')
{
\PHPUnit\Framework\Assert::assertNotNull($actual, $message);
}
/**
* Checks that condition is positive.
*
* @param $condition
* @param string $message
*/
protected function assertTrue($condition, $message = '')
{
\PHPUnit\Framework\Assert::assertTrue($condition, $message);
}
/**
* Checks that the condition is NOT true (everything but true)
*
* @param $condition
* @param string $message
*/
protected function assertNotTrue($condition, $message = '')
{
\PHPUnit\Framework\Assert::assertNotTrue($condition, $message);
}
/**
* Checks that condition is negative.
*
* @param $condition
* @param string $message
*/
protected function assertFalse($condition, $message = '')
{
\PHPUnit\Framework\Assert::assertFalse($condition, $message);
}
/**
* Checks that the condition is NOT false (everything but false)
*
* @param $condition
* @param string $message
*/
protected function assertNotFalse($condition, $message = '')
{
\PHPUnit\Framework\Assert::assertNotFalse($condition, $message);
}
/**
*
* @param $haystack
* @param $constraint
* @param string $message
*/
protected function assertThat($haystack, $constraint, $message = '')
{
\PHPUnit\Framework\Assert::assertThat($haystack, $constraint, $message);
}
/**
* Checks that haystack doesn't attend
*
* @param $haystack
* @param $constraint
* @param string $message
*/
protected function assertThatItsNot($haystack, $constraint, $message = '')
{
$constraint = new \PHPUnit\Framework\Constraint\LogicalNot($constraint);
\PHPUnit\Framework\Assert::assertThat($haystack, $constraint, $message);
}
/**
* Checks if file exists
*
* @param string $filename
* @param string $message
*/
protected function assertFileExists($filename, $message = '')
{
\PHPUnit\Framework\Assert::assertFileExists($filename, $message);
}
/**
* Checks if file doesn't exist
*
* @param string $filename
* @param string $message
*/
protected function assertFileNotExists($filename, $message = '')
{
\PHPUnit\Framework\Assert::assertFileNotExists($filename, $message);
}
/**
* @param $expected
* @param $actual
* @param $description
*/
protected function assertGreaterOrEquals($expected, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertGreaterThanOrEqual($expected, $actual, $description);
}
/**
* @param $expected
* @param $actual
* @param $description
*/
protected function assertLessOrEquals($expected, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertLessThanOrEqual($expected, $actual, $description);
}
/**
* @param $actual
* @param $description
*/
protected function assertIsEmpty($actual, $description = '')
{
\PHPUnit\Framework\Assert::assertEmpty($actual, $description);
}
/**
* @param $key
* @param $actual
* @param $description
*/
protected function assertArrayHasKey($key, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertArrayHasKey($key, $actual, $description);
}
/**
* @param $key
* @param $actual
* @param $description
*/
protected function assertArrayNotHasKey($key, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertArrayNotHasKey($key, $actual, $description);
}
/**
* Checks that array contains subset.
*
* @param array $subset
* @param array $array
* @param bool $strict
* @param string $message
*/
protected function assertArraySubset($subset, $array, $strict = false, $message = '')
{
\PHPUnit\Framework\Assert::assertArraySubset($subset, $array, $strict, $message);
}
/**
* @param $expectedCount
* @param $actual
* @param $description
*/
protected function assertCount($expectedCount, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertCount($expectedCount, $actual, $description);
}
/**
* @param $class
* @param $actual
* @param $description
*/
protected function assertInstanceOf($class, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertInstanceOf($class, $actual, $description);
}
/**
* @param $class
* @param $actual
* @param $description
*/
protected function assertNotInstanceOf($class, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertNotInstanceOf($class, $actual, $description);
}
/**
* @param $type
* @param $actual
* @param $description
*/
protected function assertInternalType($type, $actual, $description = '')
{
\PHPUnit\Framework\Assert::assertInternalType($type, $actual, $description);
}
/**
* Fails the test with message.
*
* @param $message
*/
protected function fail($message)
{
\PHPUnit\Framework\Assert::fail($message);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Codeception\Util\Shared;
trait Namespaces
{
protected function breakParts($class)
{
$class = str_replace('/', '\\', $class);
$namespaces = explode('\\', $class);
if (count($namespaces)) {
$namespaces[0] = ltrim($namespaces[0], '\\');
}
if (!$namespaces[0]) {
array_shift($namespaces);
} // remove empty namespace caused of \\
return $namespaces;
}
protected function getShortClassName($class)
{
$namespaces = $this->breakParts($class);
return array_pop($namespaces);
}
protected function getNamespaceString($class)
{
$namespaces = $this->getNamespaces($class);
return implode('\\', $namespaces);
}
protected function getNamespaceHeader($class)
{
$str = $this->getNamespaceString($class);
if (!$str) {
return "";
}
return "namespace $str;\n";
}
protected function getNamespaces($class)
{
$namespaces = $this->breakParts($class);
array_pop($namespaces);
$namespaces = array_filter($namespaces, 'strlen');
return $namespaces;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Codeception\Util;
/**
* This class is left for BC compatibility.
* Most of its contents moved to parent
*
* Class Soap
* @package Codeception\Util
*/
class Soap extends Xml
{
public static function request()
{
return new XmlBuilder();
}
public static function response()
{
return new XmlBuilder();
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Codeception\Util;
use Codeception\Lib\Notification;
use Codeception\Stub\Expected;
class Stub extends \Codeception\Stub
{
public static function never($params = null)
{
Notification::deprecate("Stub::never is deprecated in favor of \Codeception\Stub\Expected::never");
return Expected::never($params);
}
public static function once($params = null)
{
Notification::deprecate("Stub::once is deprecated in favor of \Codeception\Stub\Expected::once");
return Expected::once($params);
}
public static function atLeastOnce($params = null)
{
Notification::deprecate("Stub::atLeastOnce is deprecated in favor of \Codeception\Stub\Expected::atLeastOnce");
return Expected::atLeastOnce($params);
}
public static function exactly($count, $params = null)
{
Notification::deprecate("Stub::exactly is deprecated in favor of \Codeception\Stub\Expected::exactly");
return Expected::exactly($count, $params);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Codeception\Util;
/**
* Basic template engine used for generating initial Cept/Cest/Test files.
*/
class Template
{
protected $template;
protected $vars = [];
protected $placeholderStart;
protected $placeholderEnd;
/**
* Takes a template string
*
* @param $template
*/
public function __construct($template, $placeholderStart = '{{', $placeholderEnd = '}}')
{
$this->template = $template;
$this->placeholderStart = $placeholderStart;
$this->placeholderEnd = $placeholderEnd;
}
/**
* Replaces {{var}} string with provided value
*
* @param $var
* @param $val
* @return $this
*/
public function place($var, $val)
{
$this->vars[$var] = $val;
return $this;
}
/**
* Sets all template vars
*
* @param array $vars
*/
public function setVars(array $vars)
{
$this->vars = $vars;
}
/**
* Fills up template string with placed variables.
*
* @return mixed
*/
public function produce()
{
$result = $this->template;
$regex = sprintf('~%s([\w\.]+)%s~m', $this->placeholderStart, $this->placeholderEnd);
$matched = preg_match_all($regex, $result, $matches, PREG_SET_ORDER);
if (!$matched) {
return $result;
}
foreach ($matches as $match) { // fill in placeholders
$placeholder = $match[1];
$value = $this->vars;
foreach (explode('.', $placeholder) as $segment) {
if (is_array($value) && array_key_exists($segment, $value)) {
$value = $value[$segment];
} else {
continue 2;
}
}
$result = str_replace($this->placeholderStart . $placeholder . $this->placeholderEnd, $value, $result);
}
return $result;
}
}

View File

@@ -0,0 +1,115 @@
<?php
namespace Codeception\Util;
use GuzzleHttp\Psr7\Uri as Psr7Uri;
class Uri
{
/**
* Merges the passed $add argument onto $base.
*
* If a relative URL is passed as the 'path' part of the $add url
* array, the relative URL is mapped using the base 'path' part as
* its base.
*
* @param string $baseUri the base URL
* @param string $uri the URL to merge
* @return array the merged array
*/
public static function mergeUrls($baseUri, $uri)
{
$base = new Psr7Uri($baseUri);
$parts = parse_url($uri);
//If the relative URL does not parse, attempt to parse the entire URL.
//PHP Known bug ( https://bugs.php.net/bug.php?id=70942 )
if ($parts === false) {
$parts = parse_url($base.$uri);
}
if ($parts === false) {
throw new \InvalidArgumentException("Invalid URI $uri");
}
if (isset($parts['host']) and isset($parts['scheme'])) {
// if this is an absolute url, replace with it
return $uri;
}
if (isset($parts['host'])) {
$base = $base->withHost($parts['host']);
$base = $base->withPath('');
$base = $base->withQuery('');
$base = $base->withFragment('');
}
if (isset($parts['path'])) {
$path = $parts['path'];
$basePath = $base->getPath();
if ((strpos($path, '/') !== 0) && !empty($path)) {
if ($basePath) {
// if it ends with a slash, relative paths are below it
if (preg_match('~/$~', $basePath)) {
$path = $basePath . $path;
} else {
// remove double slashes
$dir = rtrim(dirname($basePath), '\\/');
$path = $dir . '/' . $path;
}
} else {
$path = '/' . ltrim($path, '/');
}
}
$base = $base->withPath($path);
$base = $base->withQuery('');
$base = $base->withFragment('');
}
if (isset($parts['query'])) {
$base = $base->withQuery($parts['query']);
$base = $base->withFragment('');
}
if (isset($parts['fragment'])) {
$base = $base->withFragment($parts['fragment']);
}
return (string) $base;
}
/**
* Retrieve /path?query#fragment part of URL
* @param $url
* @return string
*/
public static function retrieveUri($url)
{
$uri = new Psr7Uri($url);
return (string)(new Psr7Uri())
->withPath($uri->getPath())
->withQuery($uri->getQuery())
->withFragment($uri->getFragment());
}
public static function retrieveHost($url)
{
$urlParts = parse_url($url);
if (!isset($urlParts['host']) or !isset($urlParts['scheme'])) {
throw new \InvalidArgumentException("Wrong URL passes, host and scheme not set");
}
$host = $urlParts['scheme'] . '://' . $urlParts['host'];
if (isset($urlParts['port'])) {
$host .= ':' . $urlParts['port'];
}
return $host;
}
public static function appendPath($url, $path)
{
$uri = new Psr7Uri($url);
$cutUrl = (string)$uri->withQuery('')->withFragment('');
if ($path === '' || $path[0] === '#') {
return $cutUrl . $path;
}
return rtrim($cutUrl, '/') . '/' . ltrim($path, '/');
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Codeception\Util;
class Xml
{
/**
* @static
*
* @param \DOMDocument $xml
* @param \DOMNode $node
* @param array $array
*
* @return \DOMDocument
*/
public static function arrayToXml(\DOMDocument $xml, \DOMNode $node, $array = [])
{
foreach ($array as $el => $val) {
if (is_array($val)) {
self::arrayToXml($xml, $node->$el, $val);
} else {
$node->appendChild($xml->createElement($el, $val));
}
}
return $xml;
}
/**
* @static
*
* @param $xml
*
* @return \DOMDocument|\DOMNode
*/
public static function toXml($xml)
{
if ($xml instanceof XmlBuilder) {
return $xml->getDom();
}
if ($xml instanceof \DOMDocument) {
return $xml;
}
$dom = new \DOMDocument();
$dom->preserveWhiteSpace = false;
if ($xml instanceof \DOMNode) {
$xml = $dom->importNode($xml, true);
$dom->appendChild($xml);
return $dom;
}
if (is_array($xml)) {
return self::arrayToXml($dom, $dom, $xml);
}
if (!empty($xml)) {
$dom->loadXML($xml);
}
return $dom;
}
public static function build()
{
return new XmlBuilder();
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Codeception\Util;
/**
* That's a pretty simple yet powerful class to build XML structures in jQuery-like style.
* With no XML line actually written!
* Uses DOM extension to manipulate XML data.
*
*
* ```php
* <?php
* $xml = new \Codeception\Util\XmlBuilder();
* $xml->users
* ->user
* ->val(1)
* ->email
* ->val('davert@mail.ua')
* ->attr('valid','true')
* ->parent()
* ->cart
* ->attr('empty','false')
* ->items
* ->item
* ->val('useful item');
* ->parents('user')
* ->active
* ->val(1);
* echo $xml;
* ```
*
* This will produce this XML
*
* ```xml
* <?xml version="1.0"?>
* <users>
* <user>
* 1
* <email valid="true">davert@mail.ua</email>
* <cart empty="false">
* <items>
* <item>useful item</item>
* </items>
* </cart>
* <active>1</active>
* </user>
* </users>
* ```
*
* ### Usage
*
* Builder uses chained calls. So each call to builder returns a builder object.
* Except for `getDom` and `__toString` methods.
*
* * `$xml->node` - create new xml node and go inside of it.
* * `$xml->node->val('value')` - sets the inner value of node
* * `$xml->attr('name','value')` - set the attribute of node
* * `$xml->parent()` - go back to parent node.
* * `$xml->parents('user')` - go back through all parents to `user` node.
*
* Export:
*
* * `$xml->getDom` - get a DOMDocument object
* * `$xml->__toString` - get a string representation of XML.
*
* [Source code](https://github.com/Codeception/Codeception/blob/master/src/Codeception/Util/XmlBuilder.php)
*/
class XmlBuilder
{
/**
* @var \DOMDocument
*/
protected $__dom__;
/**
* @var \DOMElement
*/
protected $__currentNode__;
public function __construct()
{
$this->__dom__ = new \DOMDocument();
$this->__currentNode__ = $this->__dom__;
}
/**
* Appends child node
*
* @param $tag
*
* @return XmlBuilder
*/
public function __get($tag)
{
$node = $this->__dom__->createElement($tag);
$this->__currentNode__->appendChild($node);
$this->__currentNode__ = $node;
return $this;
}
/**
* @param $val
*
* @return XmlBuilder
*/
public function val($val)
{
$this->__currentNode__->nodeValue = $val;
return $this;
}
/**
* Sets attribute for current node
*
* @param $attr
* @param $val
*
* @return XmlBuilder
*/
public function attr($attr, $val)
{
$this->__currentNode__->setAttribute($attr, $val);
return $this;
}
/**
* Traverses to parent
*
* @return XmlBuilder
*/
public function parent()
{
$this->__currentNode__ = $this->__currentNode__->parentNode;
return $this;
}
/**
* Traverses to parent with $name
*
* @param $tag
*
* @return XmlBuilder
* @throws \Exception
*/
public function parents($tag)
{
$traverseNode = $this->__currentNode__;
$elFound = false;
while ($traverseNode->parentNode) {
$traverseNode = $traverseNode->parentNode;
if ($traverseNode->tagName == $tag) {
$this->__currentNode__ = $traverseNode;
$elFound = true;
break;
}
}
if (!$elFound) {
throw new \Exception("Parent $tag not found in XML");
}
return $this;
}
public function __toString()
{
return $this->__dom__->saveXML();
}
/**
* @return \DOMDocument
*/
public function getDom()
{
return $this->__dom__;
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Codeception\Util;
use Codeception\Exception\ElementNotFound;
use Codeception\Exception\MalformedLocatorException;
use Symfony\Component\CssSelector\CssSelectorConverter;
use Symfony\Component\CssSelector\Exception\ParseException;
use Codeception\Util\Soap as XmlUtils;
class XmlStructure
{
/**
* @var \DOMDocument|\DOMNode
*/
protected $xml;
public function __construct($xml)
{
$this->xml = XmlUtils::toXml($xml);
}
public function matchesXpath($xpath)
{
$path = new \DOMXPath($this->xml);
$res = $path->query($xpath);
if ($res === false) {
throw new MalformedLocatorException($xpath);
}
return $res->length > 0;
}
/**
* @param $cssOrXPath
* @return \DOMElement
*/
public function matchElement($cssOrXPath)
{
$xpath = new \DOMXpath($this->xml);
try {
$selector = (new CssSelectorConverter())->toXPath($cssOrXPath);
$els = $xpath->query($selector);
if ($els) {
return $els->item(0);
}
} catch (ParseException $e) {
}
$els = $xpath->query($cssOrXPath);
if ($els->length) {
return $els->item(0);
}
throw new ElementNotFound($cssOrXPath);
}
/**
* @param $xml
* @return bool
*/
public function matchXmlStructure($xml)
{
$xml = XmlUtils::toXml($xml);
$root = $xml->firstChild;
$els = $this->xml->getElementsByTagName($root->nodeName);
if (empty($els)) {
throw new ElementNotFound($root->nodeName, 'Element');
}
$matches = false;
foreach ($els as $node) {
$matches |= $this->matchForNode($root, $node);
}
return $matches;
}
protected function matchForNode($schema, $xml)
{
foreach ($schema->childNodes as $node1) {
$matched = false;
foreach ($xml->childNodes as $node2) {
if ($node1->nodeName == $node2->nodeName) {
$matched = $this->matchForNode($node1, $node2);
if ($matched) {
break;
}
}
}
if (!$matched) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Codeception\Module\Sequence;
if (!function_exists('sq')) {
function sq($id = null)
{
if ($id and isset(Sequence::$hash[$id])) {
return Sequence::$hash[$id];
}
$prefix = str_replace('{id}', $id, Sequence::$prefix);
$sequence = $prefix . uniqid($id);
if ($id) {
Sequence::$hash[$id] = $sequence;
}
return $sequence;
}
}
if (!function_exists('sqs')) {
function sqs($id = null)
{
if ($id and isset(Sequence::$suiteHash[$id])) {
return Sequence::$suiteHash[$id];
}
$prefix = str_replace('{id}', $id, Sequence::$prefix);
$sequence = $prefix . uniqid($id);
if ($id) {
Sequence::$suiteHash[$id] = $sequence;
}
return $sequence;
}
}