3 * Base include file for SimpleTest
5 * @subpackage WebTester
6 * @version $Id: page.php,v 1.131 2006/02/05 01:07:49 lastcraft Exp $
10 * include other SimpleTest class files
12 require_once(dirname(__FILE__
) . '/http.php');
13 require_once(dirname(__FILE__
) . '/parser.php');
14 require_once(dirname(__FILE__
) . '/tag.php');
15 require_once(dirname(__FILE__
) . '/form.php');
16 require_once(dirname(__FILE__
) . '/selector.php');
20 * Creates tags and widgets given HTML tag
23 * @subpackage WebTester
25 class SimpleTagBuilder
{
28 * Factory for the tag objects. Creates the
29 * appropriate tag object for the incoming tag name
31 * @param string $name HTML tag name.
32 * @param hash $attributes Element attributes.
33 * @return SimpleTag Tag object.
36 function createTag($name, $attributes) {
38 'a' => 'SimpleAnchorTag',
39 'title' => 'SimpleTitleTag',
40 'button' => 'SimpleButtonTag',
41 'textarea' => 'SimpleTextAreaTag',
42 'option' => 'SimpleOptionTag',
43 'label' => 'SimpleLabelTag',
44 'form' => 'SimpleFormTag',
45 'frame' => 'SimpleFrameTag');
46 $attributes = $this->_keysToLowerCase($attributes);
47 if (array_key_exists($name, $map)) {
48 $tag_class = $map[$name];
49 return new $tag_class($attributes);
50 } elseif ($name == 'select') {
51 return $this->_createSelectionTag($attributes);
52 } elseif ($name == 'input') {
53 return $this->_createInputTag($attributes);
55 return new SimpleTag($name, $attributes);
59 * Factory for selection fields.
60 * @param hash $attributes Element attributes.
61 * @return SimpleTag Tag object.
64 function _createSelectionTag($attributes) {
65 if (isset($attributes['multiple'])) {
66 return new MultipleSelectionTag($attributes);
68 return new SimpleSelectionTag($attributes);
72 * Factory for input tags.
73 * @param hash $attributes Element attributes.
74 * @return SimpleTag Tag object.
77 function _createInputTag($attributes) {
78 if (! isset($attributes['type'])) {
79 return new SimpleTextTag($attributes);
81 $type = strtolower(trim($attributes['type']));
83 'submit' => 'SimpleSubmitTag',
84 'image' => 'SimpleImageSubmitTag',
85 'checkbox' => 'SimpleCheckboxTag',
86 'radio' => 'SimpleRadioButtonTag',
87 'text' => 'SimpleTextTag',
88 'hidden' => 'SimpleTextTag',
89 'password' => 'SimpleTextTag',
90 'file' => 'SimpleUploadTag');
91 if (array_key_exists($type, $map)) {
92 $tag_class = $map[$type];
93 return new $tag_class($attributes);
99 * Make the keys lower case for case insensitive look-ups.
100 * @param hash $map Hash to convert.
101 * @return hash Unchanged values, but keys lower case.
104 function _keysToLowerCase($map) {
106 foreach ($map as $key => $value) {
107 $lower[strtolower($key)] = $value;
114 * SAX event handler. Maintains a list of
115 * open tags and dispatches them as they close.
116 * @package SimpleTest
117 * @subpackage WebTester
119 class SimplePageBuilder
extends SimpleSaxListener
{
122 var $_private_content_tag;
125 * Sets the builder up empty.
128 function SimplePageBuilder() {
129 $this->SimpleSaxListener();
133 * Frees up any references so as to allow the PHP garbage
134 * collection from unset() to work.
140 unset($this->_private_content_tags
);
144 * Reads the raw content and send events
145 * into the page to be built.
146 * @param $response SimpleHttpResponse Fetched response.
147 * @return SimplePage Newly parsed page.
150 function &parse($response) {
151 $this->_tags
= array();
152 $this->_page
= &$this->_createPage($response);
153 $parser = &$this->_createParser($this);
154 $parser->parse($response->getContent());
155 $this->_page
->acceptPageEnd();
160 * Creates an empty page.
161 * @return SimplePage New unparsed page.
164 function &_createPage($response) {
165 $page = &new SimplePage($response);
170 * Creates the parser used with the builder.
171 * @param $listener SimpleSaxListener Target of parser.
172 * @return SimpleSaxParser Parser to generate
173 * events for the builder.
176 function &_createParser(&$listener) {
177 $parser = &new SimpleHtmlSaxParser($listener);
182 * Start of element event. Opens a new tag.
183 * @param string $name Element name.
184 * @param hash $attributes Attributes without content
185 * are marked as true.
186 * @return boolean False on parse error.
189 function startElement($name, $attributes) {
190 $factory = &new SimpleTagBuilder();
191 $tag = $factory->createTag($name, $attributes);
195 if ($tag->getTagName() == 'label') {
196 $this->_page
->acceptLabelStart($tag);
197 $this->_openTag($tag);
200 if ($tag->getTagName() == 'form') {
201 $this->_page
->acceptFormStart($tag);
204 if ($tag->getTagName() == 'frameset') {
205 $this->_page
->acceptFramesetStart($tag);
208 if ($tag->getTagName() == 'frame') {
209 $this->_page
->acceptFrame($tag);
212 if ($tag->isPrivateContent() && ! isset($this->_private_content_tag
)) {
213 $this->_private_content_tag
= &$tag;
215 if ($tag->expectEndTag()) {
216 $this->_openTag($tag);
219 $this->_page
->acceptTag($tag);
224 * End of element event.
225 * @param string $name Element name.
226 * @return boolean False on parse error.
229 function endElement($name) {
230 if ($name == 'label') {
231 $this->_page
->acceptLabelEnd();
234 if ($name == 'form') {
235 $this->_page
->acceptFormEnd();
238 if ($name == 'frameset') {
239 $this->_page
->acceptFramesetEnd();
242 if ($this->_hasNamedTagOnOpenTagStack($name)) {
243 $tag = array_pop($this->_tags
[$name]);
244 if ($tag->isPrivateContent() && $this->_private_content_tag
->getTagName() == $name) {
245 unset($this->_private_content_tag
);
247 $this->_addContentTagToOpenTags($tag);
248 $this->_page
->acceptTag($tag);
255 * Test to see if there are any open tags awaiting
256 * closure that match the tag name.
257 * @param string $name Element name.
258 * @return boolean True if any are still open.
261 function _hasNamedTagOnOpenTagStack($name) {
262 return isset($this->_tags
[$name]) && (count($this->_tags
[$name]) > 0);
266 * Unparsed, but relevant data. The data is added
268 * @param string $text May include unparsed tags.
269 * @return boolean False on parse error.
272 function addContent($text) {
273 if (isset($this->_private_content_tag
)) {
274 $this->_private_content_tag
->addContent($text);
276 $this->_addContentToAllOpenTags($text);
282 * Any content fills all currently open tags unless it
283 * is part of an option tag.
284 * @param string $text May include unparsed tags.
287 function _addContentToAllOpenTags($text) {
288 foreach (array_keys($this->_tags
) as $name) {
289 for ($i = 0, $count = count($this->_tags
[$name]); $i < $count; $i++
) {
290 $this->_tags
[$name][$i]->addContent($text);
296 * Parsed data in tag form. The parsed tag is added
297 * to every open tag. Used for adding options to select
299 * @param SimpleTag $tag Option tags only.
302 function _addContentTagToOpenTags(&$tag) {
303 if ($tag->getTagName() != 'option') {
306 foreach (array_keys($this->_tags
) as $name) {
307 for ($i = 0, $count = count($this->_tags
[$name]); $i < $count; $i++
) {
308 $this->_tags
[$name][$i]->addTag($tag);
314 * Opens a tag for receiving content. Multiple tags
315 * will be receiving input at the same time.
316 * @param SimpleTag $tag New content tag.
319 function _openTag(&$tag) {
320 $name = $tag->getTagName();
321 if (! in_array($name, array_keys($this->_tags
))) {
322 $this->_tags
[$name] = array();
324 $this->_tags
[$name][] = &$tag;
329 * A wrapper for a web page.
330 * @package SimpleTest
331 * @subpackage WebTester
338 var $_left_over_labels;
340 var $_complete_forms;
343 var $_frameset_nesting_level;
344 var $_transport_error;
354 * Parses a page ready to access it's contents.
355 * @param SimpleHttpResponse $response Result of HTTP fetch.
358 function SimplePage($response = false) {
359 $this->_links
= array();
360 $this->_title
= false;
361 $this->_left_over_labels
= array();
362 $this->_open_forms
= array();
363 $this->_complete_forms
= array();
364 $this->_frameset
= false;
365 $this->_frames
= array();
366 $this->_frameset_nesting_level
= 0;
367 $this->_text
= false;
369 $this->_extractResponse($response);
371 $this->_noResponse();
376 * Extracts all of the response information.
377 * @param SimpleHttpResponse $response Response being parsed.
380 function _extractResponse($response) {
381 $this->_transport_error
= $response->getError();
382 $this->_raw
= $response->getContent();
383 $this->_sent
= $response->getSent();
384 $this->_headers
= $response->getHeaders();
385 $this->_method
= $response->getMethod();
386 $this->_url
= $response->getUrl();
387 $this->_request_data
= $response->getRequestData();
391 * Sets up a missing response.
394 function _noResponse() {
395 $this->_transport_error
= 'No page fetched yet';
397 $this->_sent
= false;
398 $this->_headers
= false;
399 $this->_method
= 'GET';
401 $this->_request_data
= false;
405 * Original request as bytes sent down the wire.
406 * @return mixed Sent content.
409 function getRequest() {
414 * Accessor for raw text of page.
415 * @return string Raw unparsed content.
423 * Accessor for plain text of page as a text browser
425 * @return string Plain text of page.
429 if (! $this->_text
) {
430 $this->_text
= SimpleHtmlSaxParser
::normalise($this->_raw
);
436 * Accessor for raw headers of page.
437 * @return string Header block as text.
440 function getHeaders() {
441 if ($this->_headers
) {
442 return $this->_headers
->getRaw();
448 * Original request method.
449 * @return string GET, POST or HEAD.
452 function getMethod() {
453 return $this->_method
;
457 * Original resource name.
458 * @return SimpleUrl Current url.
466 * Original request data.
467 * @return mixed Sent content.
470 function getRequestData() {
471 return $this->_request_data
;
475 * Accessor for last error.
476 * @return string Error from last response.
479 function getTransportError() {
480 return $this->_transport_error
;
484 * Accessor for current MIME type.
485 * @return string MIME type as string; e.g. 'text/html'
488 function getMimeType() {
489 if ($this->_headers
) {
490 return $this->_headers
->getMimeType();
496 * Accessor for HTTP response code.
497 * @return integer HTTP response code received.
500 function getResponseCode() {
501 if ($this->_headers
) {
502 return $this->_headers
->getResponseCode();
508 * Accessor for last Authentication type. Only valid
509 * straight after a challenge (401).
510 * @return string Description of challenge type.
513 function getAuthentication() {
514 if ($this->_headers
) {
515 return $this->_headers
->getAuthentication();
521 * Accessor for last Authentication realm. Only valid
522 * straight after a challenge (401).
523 * @return string Name of security realm.
526 function getRealm() {
527 if ($this->_headers
) {
528 return $this->_headers
->getRealm();
534 * Accessor for current frame focus. Will be
535 * false as no frames.
536 * @return array Always empty.
539 function getFrameFocus() {
544 * Sets the focus by index. The integer index starts from 1.
545 * @param integer $choice Chosen frame.
546 * @return boolean Always false.
549 function setFrameFocusByIndex($choice) {
554 * Sets the focus by name. Always fails for a leaf page.
555 * @param string $name Chosen frame.
556 * @return boolean False as no frames.
559 function setFrameFocus($name) {
564 * Clears the frame focus. Does nothing for a leaf page.
567 function clearFrameFocus() {
571 * Adds a tag to the page.
572 * @param SimpleTag $tag Tag to accept.
575 function acceptTag(&$tag) {
576 if ($tag->getTagName() == "a") {
577 $this->_addLink($tag);
578 } elseif ($tag->getTagName() == "title") {
579 $this->_setTitle($tag);
580 } elseif ($this->_isFormElement($tag->getTagName())) {
581 for ($i = 0; $i < count($this->_open_forms
); $i++
) {
582 $this->_open_forms
[$i]->addWidget($tag);
584 $this->_last_widget
= &$tag;
589 * Opens a label for a described widget.
590 * @param SimpleFormTag $tag Tag to accept.
593 function acceptLabelStart(&$tag) {
594 $this->_label
= &$tag;
595 unset($this->_last_widget
);
599 * Closes the most recently opened label.
602 function acceptLabelEnd() {
603 if (isset($this->_label
)) {
604 if (isset($this->_last_widget
)) {
605 $this->_last_widget
->setLabel($this->_label
->getText());
606 unset($this->_last_widget
);
608 $this->_left_over_labels
[] = SimpleTestCompatibility
::copy($this->_label
);
610 unset($this->_label
);
615 * Tests to see if a tag is a possible form
617 * @param string $name HTML element name.
618 * @return boolean True if form element.
621 function _isFormElement($name) {
622 return in_array($name, array('input', 'button', 'textarea', 'select'));
626 * Opens a form. New widgets go here.
627 * @param SimpleFormTag $tag Tag to accept.
630 function acceptFormStart(&$tag) {
631 $this->_open_forms
[] = &new SimpleForm($tag, $this->getUrl());
635 * Closes the most recently opened form.
638 function acceptFormEnd() {
639 if (count($this->_open_forms
)) {
640 $this->_complete_forms
[] = array_pop($this->_open_forms
);
645 * Opens a frameset. A frameset may contain nested
647 * @param SimpleFramesetTag $tag Tag to accept.
650 function acceptFramesetStart(&$tag) {
651 if (! $this->_isLoadingFrames()) {
652 $this->_frameset
= &$tag;
654 $this->_frameset_nesting_level++
;
658 * Closes the most recently opened frameset.
661 function acceptFramesetEnd() {
662 if ($this->_isLoadingFrames()) {
663 $this->_frameset_nesting_level
--;
668 * Takes a single frame tag and stashes it in
669 * the current frame set.
670 * @param SimpleFrameTag $tag Tag to accept.
673 function acceptFrame(&$tag) {
674 if ($this->_isLoadingFrames()) {
675 if ($tag->getAttribute('src')) {
676 $this->_frames
[] = &$tag;
682 * Test to see if in the middle of reading
684 * @return boolean True if inframeset.
687 function _isLoadingFrames() {
688 if (! $this->_frameset
) {
691 return ($this->_frameset_nesting_level
> 0);
695 * Test to see if link is an absolute one.
696 * @param string $url Url to test.
697 * @return boolean True if absolute.
700 function _linkIsAbsolute($url) {
701 $parsed = new SimpleUrl($url);
702 return (boolean
)($parsed->getScheme() && $parsed->getHost());
706 * Adds a link to the page.
707 * @param SimpleAnchorTag $tag Link to accept.
710 function _addLink($tag) {
711 $this->_links
[] = $tag;
715 * Marker for end of complete page. Any work in
716 * progress can now be closed.
719 function acceptPageEnd() {
720 while (count($this->_open_forms
)) {
721 $this->_complete_forms
[] = array_pop($this->_open_forms
);
723 foreach ($this->_left_over_labels
as $label) {
724 for ($i = 0, $count = count($this->_complete_forms
); $i < $count; $i++
) {
725 $this->_complete_forms
[$i]->attachLabelBySelector(
726 new SimpleById($label->getFor()),
733 * Test for the presence of a frameset.
734 * @return boolean True if frameset.
737 function hasFrames() {
738 return (boolean
)$this->_frameset
;
742 * Accessor for frame name and source URL for every frame that
743 * will need to be loaded. Immediate children only.
744 * @return boolean/array False if no frameset or
745 * otherwise a hash of frame URLs.
746 * The key is either a numerical
747 * base one index or the name attribute.
750 function getFrameset() {
751 if (! $this->_frameset
) {
755 for ($i = 0; $i < count($this->_frames
); $i++
) {
756 $name = $this->_frames
[$i]->getAttribute('name');
757 $url = new SimpleUrl($this->_frames
[$i]->getAttribute('src'));
758 $urls[$name ?
$name : $i +
1] = $url->makeAbsolute($this->getUrl());
764 * Fetches a list of loaded frames.
765 * @return array/string Just the URL for a single page.
768 function getFrames() {
769 $url = $this->getUrl();
770 return $url->asString();
774 * Accessor for a list of all fixed links.
775 * @return array List of urls with scheme of
776 * http or https and hostname.
779 function getAbsoluteUrls() {
781 foreach ($this->_links
as $link) {
782 if ($this->_linkIsAbsolute($link->getHref())) {
783 $all[] = $link->getHref();
790 * Accessor for a list of all relative links.
791 * @return array List of urls without hostname.
794 function getRelativeUrls() {
796 foreach ($this->_links
as $link) {
797 if (! $this->_linkIsAbsolute($link->getHref())) {
798 $all[] = $link->getHref();
805 * Accessor for URLs by the link label. Label will match
806 * regardess of whitespace issues and case.
807 * @param string $label Text of link.
808 * @return array List of links with that label.
811 function getUrlsByLabel($label) {
813 foreach ($this->_links
as $link) {
814 if ($link->getText() == $label) {
815 $matches[] = $this->_getUrlFromLink($link);
822 * Accessor for a URL by the id attribute.
823 * @param string $id Id attribute of link.
824 * @return SimpleUrl URL with that id of false if none.
827 function getUrlById($id) {
828 foreach ($this->_links
as $link) {
829 if ($link->getAttribute('id') === (string)$id) {
830 return $this->_getUrlFromLink($link);
837 * Converts a link into a target URL.
838 * @param SimpleAnchor $link Parsed link.
839 * @return SimpleUrl URL with frame target if any.
842 function _getUrlFromLink($link) {
843 $url = $this->_makeAbsolute($link->getHref());
844 if ($link->getAttribute('target')) {
845 $url->setTarget($link->getAttribute('target'));
851 * Expands expandomatic URLs into fully qualified
853 * @param SimpleUrl $url Relative URL.
854 * @return SimpleUrl Absolute URL.
857 function _makeAbsolute($url) {
858 if (! is_object($url)) {
859 $url = new SimpleUrl($url);
861 return $url->makeAbsolute($this->getUrl());
865 * Sets the title tag contents.
866 * @param SimpleTitleTag $tag Title of page.
869 function _setTitle(&$tag) {
870 $this->_title
= &$tag;
874 * Accessor for parsed title.
875 * @return string Title or false if no title is present.
878 function getTitle() {
880 return $this->_title
->getText();
886 * Finds a held form by button label. Will only
887 * search correctly built forms.
888 * @param SimpleSelector $selector Button finder.
889 * @return SimpleForm Form object containing
893 function &getFormBySubmit($selector) {
894 for ($i = 0; $i < count($this->_complete_forms
); $i++
) {
895 if ($this->_complete_forms
[$i]->hasSubmit($selector)) {
896 return $this->_complete_forms
[$i];
904 * Finds a held form by image using a selector.
905 * Will only search correctly built forms.
906 * @param SimpleSelector $selector Image finder.
907 * @return SimpleForm Form object containing
911 function &getFormByImage($selector) {
912 for ($i = 0; $i < count($this->_complete_forms
); $i++
) {
913 if ($this->_complete_forms
[$i]->hasImage($selector)) {
914 return $this->_complete_forms
[$i];
922 * Finds a held form by the form ID. A way of
923 * identifying a specific form when we have control
925 * @param string $id Form label.
926 * @return SimpleForm Form object containing the matching ID.
929 function &getFormById($id) {
930 for ($i = 0; $i < count($this->_complete_forms
); $i++
) {
931 if ($this->_complete_forms
[$i]->getId() == $id) {
932 return $this->_complete_forms
[$i];
940 * Sets a field on each form in which the field is
942 * @param SimpleSelector $selector Field finder.
943 * @param string $value Value to set field to.
944 * @return boolean True if value is valid.
947 function setField($selector, $value) {
949 for ($i = 0; $i < count($this->_complete_forms
); $i++
) {
950 if ($this->_complete_forms
[$i]->setField($selector, $value)) {
958 * Accessor for a form element value within a page.
959 * @param SimpleSelector $selector Field finder.
960 * @return string/boolean A string if the field is
961 * present, false if unchecked
962 * and null if missing.
965 function getField($selector) {
966 for ($i = 0; $i < count($this->_complete_forms
); $i++
) {
967 $value = $this->_complete_forms
[$i]->getValue($selector);