sfTestFunctionalBase.class.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. <?php
  2. require_once(dirname(__FILE__).'/../vendor/lime/lime.php');
  3. /*
  4. * This file is part of the symfony package.
  5. * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. /**
  11. * sfTestFunctional tests an application by using a browser simulator.
  12. *
  13. * @package symfony
  14. * @subpackage test
  15. * @author Fabien Potencier <fabien.potencier@symfony-project.com>
  16. * @version SVN: $Id: sfTestFunctionalBase.class.php 16170 2009-03-11 08:02:42Z fabien $
  17. */
  18. abstract class sfTestFunctionalBase
  19. {
  20. protected
  21. $testers = array(),
  22. $blockTester = null,
  23. $currentTester = null,
  24. $browser = null;
  25. protected static
  26. $test = null;
  27. /**
  28. * Initializes the browser tester instance.
  29. *
  30. * @param sfBrowserBase $browser A sfBrowserBase instance
  31. * @param lime_test $lime A lime instance
  32. */
  33. public function __construct(sfBrowserBase $browser, lime_test $lime = null, $testers = array())
  34. {
  35. $this->browser = $browser;
  36. if (is_null(self::$test))
  37. {
  38. self::$test = !is_null($lime) ? $lime : new lime_test(null, new lime_output_color());
  39. }
  40. $this->setTesters(array_merge(array(
  41. 'request' => 'sfTesterRequest',
  42. 'response' => 'sfTesterResponse',
  43. 'user' => 'sfTesterUser',
  44. ), $testers));
  45. // register our shutdown function
  46. register_shutdown_function(array($this, 'shutdown'));
  47. // register our error/exception handlers
  48. set_error_handler(array($this, 'handlePhpError'));
  49. set_exception_handler(array($this, 'handleException'));
  50. }
  51. /**
  52. * Returns the tester associated with the given name.
  53. *
  54. * @param string $name The tester name
  55. *
  56. * @param sfTester A sfTester instance
  57. */
  58. public function with($name)
  59. {
  60. if (!isset($this->testers[$name]))
  61. {
  62. throw new InvalidArgumentException(sprintf('The "%s" tester does not exist.', $name));
  63. }
  64. if ($this->blockTester)
  65. {
  66. throw new LogicException(sprintf('You cannot nest tester blocks.'));
  67. }
  68. $this->currentTester = $this->testers[$name];
  69. $this->currentTester->initialize();
  70. return $this->currentTester;
  71. }
  72. /**
  73. * Begins a block of test for the current tester.
  74. *
  75. * @param sfTester The current sfTester instance
  76. */
  77. public function begin()
  78. {
  79. if (!$this->currentTester)
  80. {
  81. throw new LogicException(sprintf('You must call with() before beginning a tester block.'));
  82. }
  83. return $this->blockTester = $this->currentTester;
  84. }
  85. /**
  86. * End a block of test for the current tester.
  87. *
  88. * @param sfTestFunctionalBase
  89. */
  90. public function end()
  91. {
  92. if (is_null($this->blockTester))
  93. {
  94. throw new LogicException(sprintf('There is not current tester block to end.'));
  95. }
  96. $this->blockTester = null;
  97. return $this;
  98. }
  99. /**
  100. * Sets the testers.
  101. *
  102. * @param array $testers An array of named testers
  103. */
  104. public function setTesters($testers)
  105. {
  106. foreach ($testers as $name => $tester)
  107. {
  108. $this->setTester($name, $tester);
  109. }
  110. }
  111. /**
  112. * Sets a tester.
  113. *
  114. * @param string $name The tester name
  115. * @param sfTester|string $tester A sfTester instance or a tester class name
  116. */
  117. public function setTester($name, $tester)
  118. {
  119. if (is_string($tester))
  120. {
  121. $tester = new $tester($this, self::$test);
  122. }
  123. if (!$tester instanceof sfTester)
  124. {
  125. throw new InvalidArgumentException(sprintf('The tester "%s" is not of class sfTester.', $name));
  126. }
  127. $this->testers[$name] = $tester;
  128. }
  129. /**
  130. * Shutdown function.
  131. *
  132. * @return void
  133. */
  134. public function shutdown()
  135. {
  136. $this->checkCurrentExceptionIsEmpty();
  137. }
  138. /**
  139. * Retrieves the lime_test instance.
  140. *
  141. * @return lime_test The lime_test instance
  142. */
  143. public function test()
  144. {
  145. return self::$test;
  146. }
  147. /**
  148. * Gets a uri.
  149. *
  150. * @param string $uri The URI to fetch
  151. * @param array $parameters The Request parameters
  152. * @param bool $changeStack Change the browser history stack?
  153. *
  154. * @return sfBrowser
  155. */
  156. public function get($uri, $parameters = array(), $changeStack = true)
  157. {
  158. return $this->call($uri, 'get', $parameters, $changeStack);
  159. }
  160. /**
  161. * Retrieves and checks an action.
  162. *
  163. * @param string $module Module name
  164. * @param string $action Action name
  165. * @param string $url Url
  166. * @param string $code The expected return status code
  167. *
  168. * @return sfTestBrowser The current sfTestBrowser instance
  169. */
  170. public function getAndCheck($module, $action, $url = null, $code = 200)
  171. {
  172. return $this->
  173. get(null !== $url ? $url : sprintf('/%s/%s', $module, $action))->
  174. with('request')->begin()->
  175. isParameter('module', $module)->
  176. isParameter('action', $action)->
  177. end()->
  178. with('response')->isStatusCode($code)
  179. ;
  180. }
  181. /**
  182. * Posts a uri.
  183. *
  184. * @param string $uri The URI to fetch
  185. * @param array $parameters The Request parameters
  186. * @param bool $changeStack Change the browser history stack?
  187. *
  188. * @return sfBrowser
  189. */
  190. public function post($uri, $parameters = array(), $changeStack = true)
  191. {
  192. return $this->call($uri, 'post', $parameters, $changeStack);
  193. }
  194. /**
  195. * Calls a request.
  196. *
  197. * @param string $uri URI to be invoked
  198. * @param string $method HTTP method used
  199. * @param array $parameters Additional paramaters
  200. * @param bool $changeStack If set to false ActionStack is not changed
  201. *
  202. * @return sfTestBrowser The current sfTestBrowser instance
  203. */
  204. public function call($uri, $method = 'get', $parameters = array(), $changeStack = true)
  205. {
  206. $this->checkCurrentExceptionIsEmpty();
  207. $uri = $this->browser->fixUri($uri);
  208. $this->test()->comment(sprintf('%s %s', strtolower($method), $uri));
  209. foreach ($this->testers as $tester)
  210. {
  211. $tester->prepare();
  212. }
  213. $this->browser->call($uri, $method, $parameters, $changeStack);
  214. return $this;
  215. }
  216. /**
  217. * Simulates deselecting a checkbox or radiobutton.
  218. *
  219. * @param string $name The checkbox or radiobutton id, name or text
  220. *
  221. * @return sfBrowser
  222. */
  223. public function deselect($name)
  224. {
  225. $this->browser->doSelect($name, false);
  226. return $this;
  227. }
  228. /**
  229. * Simulates selecting a checkbox or radiobutton.
  230. *
  231. * @param string $name The checkbox or radiobutton id, name or text
  232. *
  233. * @return sfBrowser
  234. */
  235. public function select($name)
  236. {
  237. $this->browser->doSelect($name, true);
  238. return $this;
  239. }
  240. /**
  241. * Simulates a click on a link or button.
  242. *
  243. * @param string $name The link or button text
  244. * @param array $arguments The arguments to pass to the link
  245. * @param array $options An array of options
  246. *
  247. * @return sfBrowser
  248. */
  249. public function click($name, $arguments = array(), $options = array())
  250. {
  251. list($uri, $method, $parameters) = $this->browser->doClick($name, $arguments, $options);
  252. return $this->call($uri, $method, $parameters);
  253. }
  254. /**
  255. * Simulates the browser back button.
  256. *
  257. * @return sfTestBrowser The current sfTestBrowser instance
  258. */
  259. public function back()
  260. {
  261. $this->test()->comment('back');
  262. $this->browser->back();
  263. return $this;
  264. }
  265. /**
  266. * Simulates the browser forward button.
  267. *
  268. * @return sfTestBrowser The current sfTestBrowser instance
  269. */
  270. public function forward()
  271. {
  272. $this->test()->comment('forward');
  273. $this->browser->forward();
  274. return $this;
  275. }
  276. /**
  277. * Outputs an information message.
  278. *
  279. * @param string $message A message
  280. *
  281. * @return sfTestBrowser The current sfTestBrowser instance
  282. */
  283. public function info($message)
  284. {
  285. $this->test()->info($message);
  286. return $this;
  287. }
  288. /**
  289. * Tests if the current request has been redirected.
  290. *
  291. * @deprecated since 1.2
  292. *
  293. * @param bool $boolean Flag for redirection mode
  294. *
  295. * @return sfTestBrowser The current sfTestBrowser instance
  296. */
  297. public function isRedirected($boolean = true)
  298. {
  299. return $this->with('response')->isRedirected($boolean);
  300. }
  301. /**
  302. * Checks that the current response contains a given text.
  303. *
  304. * @param string $uri Uniform resource identifier
  305. * @param string $text Text in the response
  306. *
  307. * @return sfTestBrowser The current sfTestBrowser instance
  308. */
  309. public function check($uri, $text = null)
  310. {
  311. $this->get($uri)->with('response')->isStatusCode();
  312. if ($text !== null)
  313. {
  314. $this->with('response')->contains($text);
  315. }
  316. return $this;
  317. }
  318. /**
  319. * Test an status code for the current test browser.
  320. *
  321. * @deprecated since 1.2
  322. *
  323. * @param string Status code to check, default 200
  324. *
  325. * @return sfTestBrowser The current sfTestBrowser instance
  326. */
  327. public function isStatusCode($statusCode = 200)
  328. {
  329. return $this->with('response')->isStatusCode($statusCode);
  330. }
  331. /**
  332. * Tests whether or not a given string is in the response.
  333. *
  334. * @deprecated since 1.2
  335. *
  336. * @param string Text to check
  337. *
  338. * @return sfTestBrowser The current sfTestBrowser instance
  339. */
  340. public function responseContains($text)
  341. {
  342. return $this->with('response')->contains($text);
  343. }
  344. /**
  345. * Tests whether or not a given key and value exists in the current request.
  346. *
  347. * @deprecated since 1.2
  348. *
  349. * @param string $key
  350. * @param string $value
  351. *
  352. * @return sfTestBrowser The current sfTestBrowser instance
  353. */
  354. public function isRequestParameter($key, $value)
  355. {
  356. return $this->with('request')->isParameter($key, $value);
  357. }
  358. /**
  359. * Tests for a response header.
  360. *
  361. * @deprecated since 1.2
  362. *
  363. * @param string $key
  364. * @param string $value
  365. *
  366. * @return sfTestBrowser The current sfTestBrowser instance
  367. */
  368. public function isResponseHeader($key, $value)
  369. {
  370. return $this->with('response')->isHeader($key, $value);
  371. }
  372. /**
  373. * Tests for the user culture.
  374. *
  375. * @deprecated since 1.2
  376. *
  377. * @param string $culture The user culture
  378. *
  379. * @return sfTestBrowser The current sfTestBrowser instance
  380. */
  381. public function isUserCulture($culture)
  382. {
  383. return $this->with('user')->isCulture($culture);
  384. }
  385. /**
  386. * Tests for the request is in the given format.
  387. *
  388. * @deprecated since 1.2
  389. *
  390. * @param string $format The request format
  391. *
  392. * @return sfTestBrowser The current sfTestBrowser instance
  393. */
  394. public function isRequestFormat($format)
  395. {
  396. return $this->with('request')->isFormat($format);
  397. }
  398. /**
  399. * Tests that the current response matches a given CSS selector.
  400. *
  401. * @deprecated since 1.2
  402. *
  403. * @param string $selector The response selector or a sfDomCssSelector object
  404. * @param mixed $value Flag for the selector
  405. * @param array $options Options for the current test
  406. *
  407. * @return sfTestBrowser The current sfTestBrowser instance
  408. */
  409. public function checkResponseElement($selector, $value = true, $options = array())
  410. {
  411. return $this->with('response')->checkElement($selector, $value, $options);
  412. }
  413. /**
  414. * Tests if an exception is thrown by the latest request.
  415. *
  416. * @param string $class Class name
  417. * @param string $message Message name
  418. *
  419. * @return sfTestBrowser The current sfTestBrowser instance
  420. */
  421. public function throwsException($class = null, $message = null)
  422. {
  423. $e = $this->browser->getCurrentException();
  424. if (null === $e)
  425. {
  426. $this->test()->fail('response returns an exception');
  427. }
  428. else
  429. {
  430. if (null !== $class)
  431. {
  432. $this->test()->ok($e instanceof $class, sprintf('response returns an exception of class "%s"', $class));
  433. }
  434. if (null !== $message && preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $message, $match))
  435. {
  436. if ($match[1] == '!')
  437. {
  438. $this->test()->unlike($e->getMessage(), substr($message, 1), sprintf('response exception message does not match regex "%s"', $message));
  439. }
  440. else
  441. {
  442. $this->test()->like($e->getMessage(), $message, sprintf('response exception message matches regex "%s"', $message));
  443. }
  444. }
  445. else if (null !== $message)
  446. {
  447. $this->test()->is($e->getMessage(), $message, sprintf('response exception message is "%s"', $message));
  448. }
  449. }
  450. $this->resetCurrentException();
  451. return $this;
  452. }
  453. /**
  454. * Triggers a test failure if an uncaught exception is present.
  455. *
  456. * @return bool
  457. */
  458. public function checkCurrentExceptionIsEmpty()
  459. {
  460. if (false === ($empty = $this->browser->checkCurrentExceptionIsEmpty()))
  461. {
  462. $this->test()->fail(sprintf('last request threw an uncaught exception "%s: %s"', get_class($this->browser->getCurrentException()), $this->browser->getCurrentException()->getMessage()));
  463. }
  464. return $empty;
  465. }
  466. public function __call($method, $arguments)
  467. {
  468. $retval = call_user_func_array(array($this->browser, $method), $arguments);
  469. // fix the fluent interface
  470. return $retval === $this->browser ? $this : $retval;
  471. }
  472. /**
  473. * Error handler for the current test browser instance.
  474. *
  475. * @param mixed $errno Error number
  476. * @param string $errstr Error message
  477. * @param string $errfile Error file
  478. * @param mixed $errline Error line
  479. */
  480. static public function handlePhpError($errno, $errstr, $errfile, $errline)
  481. {
  482. if (($errno & error_reporting()) == 0)
  483. {
  484. return false;
  485. }
  486. $msg = sprintf('PHP sent a "%%s" error at %s line %s (%s)', $errfile, $errline, $errstr);
  487. switch ($errno)
  488. {
  489. case E_WARNING:
  490. $msg = sprintf($msg, 'warning');
  491. throw new RuntimeException($msg);
  492. break;
  493. case E_NOTICE:
  494. $msg = sprintf($msg, 'notice');
  495. throw new RuntimeException($msg);
  496. break;
  497. case E_STRICT:
  498. $msg = sprintf($msg, 'strict');
  499. throw new RuntimeException($msg);
  500. break;
  501. case E_RECOVERABLE_ERROR:
  502. $msg = sprintf($msg, 'catchable');
  503. throw new RuntimeException($msg);
  504. break;
  505. }
  506. return false;
  507. }
  508. /**
  509. * Exception handler for the current test browser instance.
  510. *
  511. * @param Exception $exception The exception
  512. */
  513. function handleException(Exception $exception)
  514. {
  515. $this->test()->error(sprintf('%s: %s', get_class($exception), $exception->getMessage()));
  516. $traceData = $exception->getTrace();
  517. array_unshift($traceData, array(
  518. 'function' => '',
  519. 'file' => $exception->getFile() != null ? $exception->getFile() : 'n/a',
  520. 'line' => $exception->getLine() != null ? $exception->getLine() : 'n/a',
  521. 'args' => array(),
  522. ));
  523. $traces = array();
  524. $lineFormat = ' at %s%s%s() in %s line %s';
  525. for ($i = 0, $count = count($traceData); $i < $count; $i++)
  526. {
  527. $line = isset($traceData[$i]['line']) ? $traceData[$i]['line'] : 'n/a';
  528. $file = isset($traceData[$i]['file']) ? $traceData[$i]['file'] : 'n/a';
  529. $args = isset($traceData[$i]['args']) ? $traceData[$i]['args'] : array();
  530. $this->test()->error(sprintf($lineFormat,
  531. (isset($traceData[$i]['class']) ? $traceData[$i]['class'] : ''),
  532. (isset($traceData[$i]['type']) ? $traceData[$i]['type'] : ''),
  533. $traceData[$i]['function'],
  534. $file,
  535. $line
  536. ));
  537. }
  538. $this->test()->fail('An uncaught exception has been thrown.');
  539. }
  540. }