1 <?php
2
3 /**
4 * Sapphire-specific testing object designed to support functional testing of your web app. It simulates get/post
5 * requests, form submission, and can validate resulting HTML, looking up content by CSS selector.
6 *
7 * The example below shows how it works.
8 *
9 * <code>
10 * function testMyForm() {
11 * // Visit a URL
12 * $this->get("your/url");
13 *
14 * // Submit a form on the page that you get in response
15 * $this->submitForm("MyForm_ID", array("Email" => "invalid email ^&*&^"));
16 *
17 * // Validate the content that is returned
18 * $this->assertExactMatchBySelector("#MyForm_ID p.error", array("That email address is invalid."));
19 * }
20 * </code>
21 *
22 * @package sapphire
23 * @subpackage testing
24 */
25 class FunctionalTest extends SapphireTest {
26 /**
27 * Set this to true on your sub-class to disable the use of themes in this test.
28 * This can be handy for functional testing of modules without having to worry about whether a user has changed
29 * behaviour by replacing the theme.
30 */
31 static $disable_themes = false;
32
33 /**
34 * Set this to true on your sub-class to use the draft site by default for every test in this class.
35 */
36 static $use_draft_site = false;
37
38 protected $mainSession = null;
39
40 /**
41 * CSSContentParser for the most recently requested page.
42 */
43 protected $cssParser = null;
44
45 /**
46 * If this is true, then 30x Location headers will be automatically followed.
47 * If not, then you will have to manaully call $this->mainSession->followRedirection() to follow them. However, this will let you inspect
48 * the intermediary headers
49 */
50 protected $autoFollowRedirection = true;
51
52 /**
53 * Returns the {@link Session} object for this test
54 */
55 function session() {
56 return $this->mainSession->session();
57 }
58
59 function setUp() {
60 parent::setUp();
61 $this->mainSession = new TestSession();
62
63 // Disable theme, if necessary
64 if($this->stat('disable_themes')) SSViewer::set_theme(null);
65
66 // Switch to draft site, if necessary
67 if($this->stat('use_draft_site')) {
68 $this->useDraftSite();
69 }
70 }
71
72 function tearDown() {
73 parent::tearDown();
74 unset($this->mainSession);
75 }
76
77 /**
78 * Submit a get request
79 * @uses Director::test()
80 */
81 function get($url, $session = null, $headers = null, $cookies = null) {
82 $this->cssParser = null;
83 $response = $this->mainSession->get($url, $session, $headers, $cookies);
84 if($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) $response = $this->mainSession->followRedirection();
85 return $response;
86 }
87
88 /**
89 * Submit a post request
90 * @uses Director::test()
91 */
92 function post($url, $data, $headers = null, $session = null, $body = null, $cookies = null) {
93 $this->cssParser = null;
94 $response = $this->mainSession->post($url, $data, $headers, $session, $body, $cookies);
95 if($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) $response = $this->mainSession->followRedirection();
96 return $response;
97 }
98
99 /**
100 * Submit the form with the given HTML ID, filling it out with the given data.
101 * Acts on the most recent response
102 */
103 function submitForm($formID, $button = null, $data = array()) {
104 $this->cssParser = null;
105 $response = $this->mainSession->submitForm($formID, $button, $data);
106 if($this->autoFollowRedirection && is_object($response) && $response->getHeader('Location')) $response = $this->mainSession->followRedirection();
107 return $response;
108 }
109
110 /**
111 * Return the most recent content
112 */
113 function content() {
114 return $this->mainSession->lastContent();
115 }
116
117 /**
118 * Find an attribute in a SimpleXMLElement object by name.
119 * @param SimpleXMLElement object
120 * @param string $attribute Name of attribute to find
121 * @return SimpleXMLElement object of the attribute
122 */
123 function findAttribute($object, $attribute) {
124 $found = false;
125 foreach($object->attributes() as $a => $b) {
126 if($a == $attribute) {
127 $found = $b;
128 }
129 }
130 return $found;
131 }
132
133 /**
134 * Return a CSSContentParser for the most recent content.
135 */
136 function cssParser() {
137 if(!$this->cssParser) $this->cssParser = new CSSContentParser($this->mainSession->lastContent());
138 return $this->cssParser;
139 }
140
141 /**
142 * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
143 * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag
144 * will be examined. The assertion fails if one of the expectedMatches fails to appear.
145 *
146 * Note: characters are stripped from the content; make sure that your assertions take this into account.
147 *
148 * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
149 * @param array|string $expectedMatches The content of at least one of the matched tags
150 * @throws PHPUnit_Framework_AssertionFailedError
151 * @return boolean
152 */
153 function assertPartialMatchBySelector($selector, $expectedMatches) {
154 if(is_string($expectedMatches)) $expectedMatches = array($expectedMatches);
155
156 $items = $this->cssParser()->getBySelector($selector);
157
158 $actuals = array();
159 if($items) foreach($items as $item) $actuals[trim(preg_replace("/[ \n\r\t]+/", " ", $item. ''))] = true;
160
161 foreach($expectedMatches as $match) {
162 if(!isset($actuals[$match])) {
163 throw new PHPUnit_Framework_AssertionFailedError(
164 "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" . implode("'\n'", $expectedMatches) . "'\n\n"
165 . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"
166 );
167 return false;
168 }
169 }
170
171 return true;
172 }
173
174 /**
175 * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
176 * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag
177 * will be examined. The assertion fails if one of the expectedMatches fails to appear.
178 *
179 * Note: characters are stripped from the content; make sure that your assertions take this into account.
180 *
181 * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
182 * @param array|string $expectedMatches The content of *all* matching tags as an array
183 * @throws PHPUnit_Framework_AssertionFailedError
184 * @return boolean
185 */
186 function assertExactMatchBySelector($selector, $expectedMatches) {
187 if(is_string($expectedMatches)) $expectedMatches = array($expectedMatches);
188
189 $items = $this->cssParser()->getBySelector($selector);
190
191 $actuals = array();
192 if($items) foreach($items as $item) $actuals[] = trim(preg_replace("/[ \n\r\t]+/", " ", $item. ''));
193
194 if($expectedMatches != $actuals) {
195 throw new PHPUnit_Framework_AssertionFailedError(
196 "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" . implode("'\n'", $expectedMatches) . "'\n\n"
197 . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"
198 );
199 return false;
200 }
201
202 return true;
203 }
204
205 /**
206 * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
207 * The given CSS selector will be applied to the HTML of the most recent page. The content of every matching tag
208 * will be examined. The assertion fails if one of the expectedMatches fails to appear.
209 *
210 * Note: characters are stripped from the content; make sure that your assertions take this into account.
211 *
212 * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
213 * @param array|string $expectedMatches The content of at least one of the matched tags
214 * @throws PHPUnit_Framework_AssertionFailedError
215 * @return boolean
216 */
217 function assertPartialHTMLMatchBySelector($selector, $expectedMatches) {
218 if(is_string($expectedMatches)) $expectedMatches = array($expectedMatches);
219
220 $items = $this->cssParser()->getBySelector($selector);
221
222 $actuals = array();
223 if($items) foreach($items as $item) $actuals[$item->asXML()] = true;
224
225 foreach($expectedMatches as $match) {
226 if(!isset($actuals[$match])) {
227 throw new PHPUnit_Framework_AssertionFailedError(
228 "Failed asserting the CSS selector '$selector' has a partial match to the expected elements:\n'" . implode("'\n'", $expectedMatches) . "'\n\n"
229 . "Instead the following elements were found:\n'" . implode("'\n'", array_keys($actuals)) . "'"
230 );
231 return false;
232 }
233 }
234
235 return true;
236 }
237
238 /**
239 * Assert that the most recently queried page contains a number of content tags specified by a CSS selector.
240 * The given CSS selector will be applied to the HTML of the most recent page. The full HTML of every matching tag
241 * will be examined. The assertion fails if one of the expectedMatches fails to appear.
242 *
243 * Note: characters are stripped from the content; make sure that your assertions take this into account.
244 *
245 * @param string $selector A basic CSS selector, e.g. 'li.jobs h3'
246 * @param array|string $expectedMatches The content of *all* matched tags as an array
247 * @throws PHPUnit_Framework_AssertionFailedError
248 * @return boolean
249 */
250 function assertExactHTMLMatchBySelector($selector, $expectedMatches) {
251 $items = $this->cssParser()->getBySelector($selector);
252
253 $actuals = array();
254 if($items) foreach($items as $item) $actuals[] = $item->asXML();
255
256 if($expectedMatches != $actuals) {
257 throw new PHPUnit_Framework_AssertionFailedError(
258 "Failed asserting the CSS selector '$selector' has an exact match to the expected elements:\n'" . implode("'\n'", $expectedMatches) . "'\n\n"
259 . "Instead the following elements were found:\n'" . implode("'\n'", $actuals) . "'"
260 );
261 }
262 }
263
264 /**
265 * Log in as the given member
266 * @param $member The ID, fixture codename, or Member object of the member that you want to log in
267 */
268 function logInAs($member) {
269 if(is_object($member)) $memberID = $member->ID;
270 elseif(is_numeric($member)) $memberID = $member;
271 else $memberID = $this->idFromFixture('Member', $member);
272
273 $this->session()->inst_set('loggedInAs', $memberID);
274 }
275
276 /**
277 * Use the draft (stage) site for testing.
278 * This is helpful if you're not testing publication functionality and don't want "stage management" cluttering your test.
279 */
280 function useDraftSite() {
281 $this->session()->inst_set('readingMode', 'Stage.Stage');
282 $this->session()->inst_set('unsecuredDraftSite', true);
283 }
284
285 /**
286 * Return a static variable from this class.
287 * Gets around PHP's lack of late static binding.
288 */
289 function stat($varName) {
290 $className = get_class($this);
291 return eval("return {$className}::\$$varName;");
292 }
293 }
294