From b2330db6062a8b8a3def537db9d152eca78a8c36 Mon Sep 17 00:00:00 2001 From: tjhunt Date: Fri, 12 Jun 2009 03:13:29 +0000 Subject: [PATCH] ajaxlib: MDL-16695 New page_requirements_manager class. Soon to replace require_js. This new class does the work that require_js used to do, and more. It can track a number of different things that may be required to be output somewhere on the page, including: * Links to JS files * Links to CSS files * Links to YUI libraries (this class knows about the dependancies between the different libs). * Skip links that go from the top of to various places in the content. * Calls to JavaScript functions (for example to initialise things) * Bits of data from PHP that need to be available to JavaScript * As a special case of that, an easy way to pass language strings to JS. The new API looks like $PAGE->requires->css('mod/mymod/styles.css'); $PAGE->requires->js('mod/mymod/script.js'); $PAGE->requires->js('mod/mymod/small_but_urgent.js')->in_head(); $PAGE->requires->js_function_call('init_mymod', array($data))->on_dom_ready(); $PAGE->requires is the canonical instances of this new class. The commit also includes unit tests, and hopefully the PHP doc comments are clear enough that it is easy to understand. --- lang/en_utf8/access.php | 1 + lib/ajax/ajaxlib.php | 1100 ++++++++++++++++++++++++++- lib/ajax/simpletest/testajaxlib.php | 417 +++++++++- lib/pagelib.php | 15 + 4 files changed, 1501 insertions(+), 32 deletions(-) diff --git a/lang/en_utf8/access.php b/lang/en_utf8/access.php index 0331636991..02fb0ac7b1 100644 --- a/lang/en_utf8/access.php +++ b/lang/en_utf8/access.php @@ -21,6 +21,7 @@ $string['sitemap'] = 'Site map'; $string['skipa'] = 'Skip $a'; $string['skipblock'] = 'Skip block'; $string['skipnavigation'] = 'Skip navigation'; +$string['skipto'] = 'Skip to $a'; $string['tabledata'] = 'Data table, $a'; $string['tablelayout'] = 'Layout table, $a'; $string['tocontent'] = 'Skip to main content'; diff --git a/lib/ajax/ajaxlib.php b/lib/ajax/ajaxlib.php index 06b526b947..e0aeaa2c89 100644 --- a/lib/ajax/ajaxlib.php +++ b/lib/ajax/ajaxlib.php @@ -1,7 +1,1105 @@ . + + +/** + * Library functions to facilitate the use of JavaScript in Moodle. + * + * @package moodlecore + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +/** + * Initialise a page_requirements_manager with the bits of JavaScript that every + * Moodle page needs. + * + * @param page_requirements_manager $requires The page_requirements_manager to initialise. + */ +function setup_core_javascript(page_requirements_manager $requires) { + global $CFG; + + // JavaScript should always work with $CFG->httpswwwroot rather than $CFG->wwwroot. + // Otherwise, in some situations, users will get warnings about insecure content + // on sercure pages from their web browser. + + $config = array( + 'wwwroot' => $CFG->httpswwwroot, // Yes, really. See above. + 'pixpath' => $CFG->pixpath, + 'modpixpath' => $CFG->modpixpath, + 'sesskey' => sesskey(), + ); + if (debugging('', DEBUG_DEVELOPER)) { + $config['developerdebug'] = true; + } + $requires->data_for_js('moodle_cfg', $config)->in_head(); + + // Note that, as a short-cut, the code + // $js = "document.body.className += ' jsenabled';\n"; + // is hard-coded in @see{page_requirements_manager::get_top_of_body_code}. +} + + /** - * Library functions for using AJAX with Moodle. + * This class tracks all the things that are needed by the current page. + * + * Normally, the only instance of this class you will need to work with is the + * one accessible via $PAGE->requires. + * + * Typical useage would be + * $PAGE->requires->css('mod/mymod/styles.css'); + * $PAGE->requires->js('mod/mymod/script.js'); + * $PAGE->requires->js('mod/mymod/small_but_urgent.js')->in_head(); + * $PAGE->requires->js_function_call('init_mymod', array($data))->on_dom_ready(); + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +class page_requirements_manager { + const WHEN_IN_HEAD = 0; + const WHEN_TOP_OF_BODY = 10; + const WHEN_AT_END = 20; + const WHEN_ON_DOM_READY = 30; + + protected $linkedrequiremets = array(); + protected $stringsforjs = array(); + protected $requiredjscode = array(); + + protected $variablesinitialised = array('mstr' => 1); // 'mstr' is special. See string_for_js. + + protected $headdone = false; + protected $topofbodydone = false; + + /** + * Ensure that the specified script file is linked to from this page. By default + * the link is put at the end of the page, since this gives best load performance. + * + * Even if a particular script is requested more than once, it will only be linked + * to once. + * + * @param $jsfile The path to the .js file, relative to $CFG->dirroot / $CFG->wwwroot. + * No leading slash. For example 'mod/mymod/customscripts.js'; + * @param boolean $fullurl This parameter is intended for internal use only. + * (If true, $jsfile is treaded as a full URL, not relative $CFG->wwwroot.) + * @return required_js A required_js object. This allows you to control when the + * link to the script is output by calling methods like ->asap() or + * ->in_head(). + */ + public function js($jsfile, $fullurl = false) { + global $CFG; + if (!$fullurl) { + if (!file_exists($CFG->dirroot . '/' . $jsfile)) { + throw new coding_exception('Attept to require a JavaScript file that does not exist.', $jsfile); + } + $url = $CFG->httpswwwroot . '/' . $jsfile; + } else { + $url = $jsfile; + } + if (!isset($this->linkedrequiremets[$url])) { + $this->linkedrequiremets[$url] = new required_js($this, $url); + } + return $this->linkedrequiremets[$url]; + } + + /** + * Ensure that the specified YUI library file, and all its required dependancies, + * are linked to from this page. By default the link is put at the end of the + * page, since this gives best load performance. Optional dependencies are not + * loaded automatically - if you want them you will need to load them first with + * other calls to this method. + * + * If the YUI library you ask for requires one or more CSS files, and if + * <head> has already been printed, then an exception will be thrown. + * + * Even if a particular library is requested more than once (perhaps as a dependancy + * of other libraries) it will only be linked to once. + * + * @param $libname the name of the YUI library you require. For example 'autocomplete'. + * @return required_yui_lib A requried_yui_lib object. This allows you to control when the + * link to the script is output by calling methods like ->asap() or + * ->in_head(). + */ + public function yui_lib($libname) { + $key = 'yui:' . $libname; + if (!isset($this->linkedrequiremets[$key])) { + $this->linkedrequiremets[$key] = new required_yui_lib($this, $libname); + } + return $this->linkedrequiremets[$key]; + } + + /** + * Ensure that the specified CSS file is linked to from this page. Because + * stylesheet links must go in the <head> part of the HTML, you must call + * this function before get_head_code is called. That normally means before + * the call to print_header. If you call it when it is too late, an exception + * will be thrown. + * + * Even if a particular style sheet is requested more than once, it will only + * be linked to once. + * + * @param string $stylesheet The path to the .js file, relative to + * $CFG->dirroot / $CFG->wwwroot. No leading slash. For example + * 'mod/mymod/styles.css'; + * @param boolean $fullurl This parameter is intended for internal use only. + * (If true, $stylesheet is treaded as a full URL, not relative $CFG->wwwroot.) + */ + public function css($stylesheet, $fullurl = false) { + global $CFG; + + if ($this->headdone) { + throw new coding_exception('Cannot require a CSS file after has been printed.', $stylesheet); + } + if (!$fullurl) { + if (!file_exists($CFG->dirroot . '/' . $stylesheet)) { + throw new coding_exception('Attept to require a CSS file that does not exist.', $stylesheet); + } + $url = $CFG->httpswwwroot . '/' . $stylesheet; + } else { + $url = $stylesheet; + } + if (!isset($this->linkedrequiremets[$url])) { + $this->linkedrequiremets[$url] = new required_css($this, $url); + } + } + + /** + * Ensure that a skip link to a given target is printed at the top of the <body>. + * You must call this function before get_top_of_body_code, (if not, an exception + * will be thrown). That normally means you must call this before the call to print_header. + * + * If you ask for a particular skip link to be printed, it is then your responsibility + * to ensure that the appropraite <a name="..."> tag is printed in the body of the + * page, so that the skip link goes somewhere. + * + * Even if a particular skip link is requested more than once, only one copy of it will be output. + * + * @param $target the name of anchor this link should go to. For example 'maincontent'. + * @param $linktext The text to use for the skip link. Normally get_string('skipto', 'access', ...); + */ + public function skip_link_to($target, $linktext) { + if (!isset($this->linkedrequiremets[$target])) { + $this->linkedrequiremets[$target] = new required_skip_link($this, $target, $linktext); + } + } + + /** + * Ensure that the specified JavaScript function is called from an inline script + * somewhere on this page. By default the call will be put in a script tag at the + * end of the page, since this gives best page-load performance. + * + * If you request that a particular function is called several times, then + * that is what will happen (unlike linking to a CSS or JS file, where only + * one link will be output). + * + * @param string $function the name of the JavaScritp function to call. Can + * be a compound name like 'YAHOO.util.Event.addListener'. + * @param array $arguments and array of arguments to be passed to the function. + * When generating the function call, this will be escaped using json_encode, + * so passing objects and arrays should work. + * @return required_js_function_call A required_js_function_call object. + * This allows you to control when the link to the script is output by + * calling methods like ->asap(), ->in_head(), ->at_top_of_body() or + * ->on_dom_ready() methods. + */ + public function js_function_call($function, $arguments = array()) { + $requirement = new required_js_function_call($this, $function, $arguments); + $this->requiredjscode[] = $requirement; + return $requirement; + } + + /** + * Make a language string available to JavaScript. All the strings will be + * available in a mstr object in the global namespace. So, for example, + * after a call to $PAGE->requires->string_for_js('course', 'moodle'); + * then the JavaScript variable mstr.moodle.course will be 'Course', or the + * equivalent in the current language. + * + * The arguments to this function are just like the arguments to get_string + * except that $module is not optional, and there are limitations on how you + * use $a. Because each string is only stored once in the JavaScript (based + * on $identifier and $module) you cannot get the same string with two different + * values of $a. If you try, an exception will be thrown. + * + * If you do need the same string expanded with different $a values, then + * the solution is to put them in your own data structure (e.g. and array) + * that you pass to JavaScript with @see{data_for_js}. + * + * @param string $identifier the desired string. + * @param string $module the language file to look in. + * @param mixed $a any extra data to add into the string (optional). + */ + public function string_for_js($identifier, $module, $a = NULL) { + $string = get_string($identifier, $module, $a); + if (!$module) { + $module = 'moodle'; + } + if (isset($this->stringsforjs[$module][$identifier]) && $this->stringsforjs[$module][$identifier] != $string) { + throw new coding_exception("Attempt to re-define already required string '$identifier' " . + "from lang file '$module'. Did you already ask for it with a different \$a?"); + } + $this->stringsforjs[$module][$identifier] = $string; + } + + /** + * Make some data from PHP available to JavaScript code. For example, if you call + * $PAGE->requires->data_for_js('mydata', array('name' => 'Moodle')); + * then in JavsScript mydata.name will be 'Moodle'. + * + * You cannot call this function more than once with the same variable name + * (if you try, it will throw an exception). Your code should prepare all the + * date you want, and then pass it to this method. There is no way to change + * the value associated with a particular variable later. + * + * @param string $variable the the name of the JavaScript variable to assign the data to. + * Will probably work if you use a compound name like 'mybuttons.button[1]', but this + * should be considered an experimental feature. + * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode, + * so passing objects and arrays should work. + * @return required_data_for_js A required_data_for_js object. + * This allows you to control when the link to the script is output by + * calling methods like ->asap(), ->in_head(), ->at_top_of_body() or + * ->on_dom_ready() methods. + */ + public function data_for_js($variable, $data) { + if (isset($this->variablesinitialised[$variable])) { + throw new coding_exception("A variable called '" . $variable . + "' has already been passed ot JavaScript. You cannot overwrite it."); + } + $requirement = new required_data_for_js($this, $variable, $data); + $this->requiredjscode[] = $requirement; + $this->variablesinitialised[$variable] = 1; + return $requirement; + } + + /** + * Get the code for the linked resources that need to appear in a particular place. + * @param $when one of the WHEN_... constants. + * @return string the HTML that should be output in that place. + */ + protected function get_linked_resources_code($when) { + $output = ''; + foreach ($this->linkedrequiremets as $requirement) { + if (!$requirement->is_done() && $requirement->get_when() == $when) { + $output .= $requirement->get_html(); + $requirement->mark_done(); + } + } + return $output; + } + + /** + * Get the code for the linked resources that need to appear in a particular place. + * @param $when one of the WHEN_... constants. + * @return string the javascript that should be output in that place. + */ + protected function get_javascript_code($when, $indent = '') { + $output = ''; + foreach ($this->requiredjscode as $requirement) { + if (!$requirement->is_done() && $requirement->get_when() == $when) { + $output .= $indent . $requirement->get_js_code(); + $requirement->mark_done(); + } + } + return $output; + } + + /** + * Generate any HTML that needs to go inside the <head> tag. + * + * @return string the HTML code to to inside the <head> tag. + */ + public function get_head_code() { + $output = $this->get_linked_resources_code(self::WHEN_IN_HEAD); + $js = $this->get_javascript_code(self::WHEN_IN_HEAD); + $output .= ajax_generate_script_tag($js); + $this->headdone = true; + return $output; + } + + /** + * Generate any HTML that needs to go at the start of the <body> tag. + * + * @return string the HTML code to go at the start of the <body> tag. + */ + public function get_top_of_body_code() { + $output = $this->get_linked_resources_code(self::WHEN_TOP_OF_BODY); + $js = "document.body.className += ' jsenabled';\n"; + $js .= $this->get_javascript_code(self::WHEN_TOP_OF_BODY); + $output .= ajax_generate_script_tag($js); + $this->topofbodydone = true; + return $output; + } + + /** + * Generate any HTML that needs to go at the end of the page. + * + * @return string the HTML code to to at the end of the page. + */ + public function get_end_code() { + $output = $this->get_linked_resources_code(self::WHEN_AT_END); + + array_unshift($this->requiredjscode, new required_data_for_js($this, 'mstr', $this->stringsforjs)); + $js = $this->get_javascript_code(self::WHEN_AT_END); + + $ondomreadyjs = $this->get_javascript_code(self::WHEN_ON_DOM_READY, ' '); + if ($ondomreadyjs) { + $js .= "YAHOO.util.Event.onDOMReady(function() {\n" . $ondomreadyjs . "});\n"; + } + + $output .= ajax_generate_script_tag($js); + + return $output; + } + + /** + * Have we already output the code in the tag? + * + * @return boolean + */ + public function is_head_done() { + return $this->headdone; + } + + /** + * Have we already output the code at the start of the tag? + * + * @return boolean + */ + public function is_top_of_body_done() { + return $this->topofbodydone; + } +} + + +/** + * This is the base class for all sorts of requirements. just to factor out some + * common code. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class requirement_base { + protected $manager; + protected $when; + protected $done = false; + + /** + * Constructor. Normally the class and its subclasses should not be created + * directly. Client code should create them via a page_requirements_manager + * method like ->js(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + */ + protected function __construct(page_requirements_manager $manager) { + $this->manager = $manager; + } + + /** + * Mark that this requirement has been satisfied (that is, that the HTML + * returned by @see{get_html} has been output. + * @return boolean has this requirement been satisfied yet? That is, has + * that the HTML returned by @see{get_html} has been output already. + */ + public function is_done() { + return $this->done; + } + + /** + * Mark that this requirement has been satisfied (that is, that the HTML + * returned by @see{get_html} has been output. + */ + public function mark_done() { + $this->done = true; + } + + /** + * Where on the page the HTML this requirement is meant to go. + * @return integer One of the page_requirements_manager::WHEN_... constants. + */ + public function get_when() { + return $this->when; + } +} + +/** + * This class represents something that must be output somewhere in the HTML. + * + * Examples include links to JavaScript or CSS files. However, it should not + * necessarily be output immediately, we may have to wait for an appropriate time. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class linked_requirement extends requirement_base { + protected $url; + + /** + * Constructor. Normally the class and its subclasses should not be created + * directly. Client code should create them via a page_requirements_manager + * method like ->js(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $url The URL of the thing we are linking to. + */ + protected function __construct(page_requirements_manager $manager, $url) { + parent::__construct($manager); + $this->url = $url; + } + + /** + * @return string the HTML needed to satisfy this requirement. + */ + abstract public function get_html(); +} + + +/** + * A subclass of @see{linked_requirement} to represent a requried JavaScript file. + * + * You should not create instances of this class directly. Instead you should + * work with a @see{page_requirements_manager} - and probably the only + * page_requirements_manager you will ever need is the one at $PAGE->requires. + * + * The methods ->asap, ->in_head and at_top_of_body() are indented to be used + * as a fluid API, so you can say things like + * $PAGE->requires->js('mod/mymod/script.js')->in_head(); + * + * However, by default JavaScript files are included at the end of the HTML. + * This is recommended practice because it means that the web browser will only + * start loading the javascript files after the rest of the page is loaded, and + * that gives the best performance for users. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_js extends linked_requirement { + /** + * Constructor. Normally instances of this class should not be created + * directly. Client code should create them via the page_requirements_manager + * method ->js(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $url The URL of the JavaScript file we are linking to. + */ + public function __construct(page_requirements_manager $manager, $url) { + parent::__construct($manager, $url); + $this->when = page_requirements_manager::WHEN_AT_END; + } + + public function get_html() { + return '' . "\n"; + } + + /** + * Indicate that the link to this JavaScript file should be output as soon as + * possible. That is, if this requirement has already been output, this method + * does nothing. Otherwise, if the <head> tag has not yet been printed, the link + * to this script will be put in <head>. Otherwise, this method returns a + * fragment of HTML that the caller is responsible for outputting as soon as + * possible. In fact, it is recommended that you only call this function from + * an echo statement, like: + * echo $PAGE->requires->js(...)->asap(); + * + * @return string The HTML required to include this JavaScript file. The caller + * is responsible for outputting this HTML promptly. + */ + public function asap() { + if ($this->is_done()) { + return; + } + if (!$this->manager->is_head_done()) { + $this->in_head(); + return ''; + } + $ouput = $this->get_html(); + $this->mark_done(); + return $ouput; + } + + /** + * Indicate that the link to this JavaScript file should be output in the + * <head> section of the HTML. If it too late for this request to be + * satisfied, an exception is thrown. + */ + public function in_head() { + if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_IN_HEAD) { + return; + } + if ($this->manager->is_head_done()) { + throw new coding_exception('Too late to ask for a JavaScript file to be linked to from <head>.'); + } + $this->when = page_requirements_manager::WHEN_IN_HEAD; + } + + /** + * Indicate that the link to this JavaScript file should be output at the top + * of the <body> section of the HTML. If it too late for this request to be + * satisfied, an exception is thrown. + */ + public function at_top_of_body() { + if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_TOP_OF_BODY) { + return; + } + if ($this->manager->is_top_of_body_done()) { + throw new coding_exception('Too late to ask for a JavaScript file to be linked to from the top of <body>.'); + } + $this->when = page_requirements_manager::WHEN_TOP_OF_BODY; + } +} + + +/** + * A subclass of @see{linked_requirement} to represent a requried YUI library. + * + * You should not create instances of this class directly. Instead you should + * work with a @see{page_requirements_manager} - and probably the only + * page_requirements_manager you will ever need is the one at $PAGE->requires. + * + * The methods ->asap, ->in_head and at_top_of_body() are indented to be used + * as a fluid API, so you can say things like + * $PAGE->requires->yui_lib('autocomplete')->in_head(); + * + * This class (with the help of @see{ajax_resolve_yui_lib}) knows about the + * dependancies between the different YUI libraries, and will include all the + * other libraries required by the one you ask for. It also knows which YUI + * libraries require css files. If the library you ask for requires CSS files, + * then you should ask for it before <head> is output, or an exception will + * be thrown. + * + * By default JavaScript files are included at the end of the HTML. + * This is recommended practice because it means that the web browser will only + * start loading the javascript files after the rest of the page is loaded, and + * that gives the best performance for users. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_yui_lib extends linked_requirement { + protected $jss = array(); + protected $cssurls; + + /** + * Constructor. Normally instances of this class should not be created + * directly. Client code should create them via the page_requirements_manager + * method ->yui_lib(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $libname The name of the YUI library you want. See the array + * defined in @see{ajax_resolve_yui_lib} for a list of known libraries. + */ + public function __construct(page_requirements_manager $manager, $libname) { + parent::__construct($manager, ''); + $this->when = page_requirements_manager::WHEN_AT_END; + + list($jsurls, $this->cssurls) = ajax_resolve_yui_lib($libname); + foreach ($jsurls as $jsurl) { + $this->jss[] = $manager->js($jsurl, true); + } + foreach ($this->cssurls as $cssurl) { + $manager->css($cssurl, true); + } + if (!empty($this->cssurls)) { + global $PAGE; + $page->add_body_class('yui-skin-sam'); + } + } + + public function get_html() { + // Since we create a required_js for each of our files, that will generate the HTML. + return ''; + } + + /** + * Indicate that the link to this YUI library file should be output as soon as + * possible. The comment above @see{required_js::asap} applies to this method too. + * + * @return string The HTML required to include this JavaScript file. The caller + * is responsible for outputting this HTML promptly. For example, a good way to + * call this method is like + * echo $PAGE->requires->yui_lib(...)->asap(); + */ + public function asap() { + if ($this->is_done()) { + return; + } + + if (!$this->manager->is_head_done()) { + $this->in_head(); + return ''; + } + + $ouput = ''; + foreach ($this->jss as $requiredjs) { + $ouput .= $requiredjs->immediately(); + } + $this->mark_done(); + return $ouput; + } + + /** + * Indicate that the links to this YUI library should be output in the + * <head> section of the HTML. If it too late for this request to be + * satisfied, an exception is thrown. + */ + public function in_head() { + if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_IN_HEAD) { + return; + } + + if ($this->manager->is_head_done()) { + throw new coding_exception('Too late to ask for a YUI library to be linked to from <head>.'); + } + + $this->when = page_requirements_manager::WHEN_IN_HEAD; + foreach ($this->jss as $requiredjs) { + $ouput .= $requiredjs->in_head(); + } + } + + /** + * Indicate that the links to this YUI library should be output in the + * <head> section of the HTML. If it too late for this request to be + * satisfied, an exception is thrown. + */ + public function at_top_of_body() { + if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_TOP_OF_BODY) { + return; + } + + if ($this->manager->is_top_of_body_done()) { + throw new coding_exception('Too late to ask for a YUI library to be linked to from the top of <body>.'); + } + + $this->when = page_requirements_manager::WHEN_TOP_OF_BODY; + foreach ($this->jss as $requiredjs) { + $ouput .= $requiredjs->at_top_of_body(); + } + } +} + + +/** + * A subclass of @see{linked_requirement} to represent a required CSS file. + * Of course, all links to CSS files must go in the <head section of the HTML. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_css extends linked_requirement { + /** + * Constructor. Normally instances of this class should not be created + * directly. Client code should create them via the page_requirements_manager + * method ->css(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $url The URL of the CSS file we are linking to. + */ + public function __construct(page_requirements_manager $manager, $url) { + parent::__construct($manager, $url); + $this->when = page_requirements_manager::WHEN_IN_HEAD; + } + + public function get_html() { + return '' . "\n";; + } +} + + +/** + * A subclass of @see{linked_requirement} to represent a skip link. + * A skip link is a concept from accessibility. You have some links like + * 'Skip to main content' linking to an #maincontent anchor, at the start of the + * <body> tag, so that users using assistive technologies like screen readers + * can easily get to the main content without having to work their way through + * any navigation, blocks, etc. that comes before it in the HTML. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_skip_link extends linked_requirement { + protected $linktext; + + /** + * Constructor. Normally instances of this class should not be created + * directly. Client code should create them via the page_requirements_manager + * method ->yui_lib(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $target the name of the anchor in the page we are linking to. + * @param string $linktext the test to use for the link. + */ + public function __construct(page_requirements_manager $manager, $target, $linktext) { + parent::__construct($manager, $target); + $this->when = page_requirements_manager::WHEN_TOP_OF_BODY; + $this->linktext = $linktext; + } + + public function get_html() { + return '\n"; + } +} + + +/** + * This is the base class for requirements that are JavaScript code. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class required_js_code extends requirement_base { + + /** + * Constructor. Normally the class and its subclasses should not be created + * directly. Client code should create them via a page_requirements_manager + * method like ->js(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + */ + protected function __construct(page_requirements_manager $manager) { + parent::__construct($manager); + $this->when = page_requirements_manager::WHEN_AT_END; + } + + /** + * @return string the JavaScript code needed to satisfy this requirement. + */ + abstract public function get_js_code(); + + /** + * Indicate that the link to this JavaScript file should be output as soon as + * possible. That is, if this requirement has already been output, this method + * does nothing. Otherwise, if the <head> tag has not yet been printed, the link + * to this script will be put in <head>. Otherwise, this method returns a + * fragment of HTML that the caller is responsible for outputting as soon as + * possible. In fact, it is recommended that you only call this function from + * an echo statement, like: + * echo $PAGE->requires->js(...)->asap(); + * + * @return string The HTML required to include this JavaScript file. The caller + * is responsible for outputting this HTML promptly. + */ + public function asap() { + if ($this->is_done()) { + return; + } + if (!$this->manager->is_head_done()) { + $this->in_head(); + return ''; + } + $js = $this->get_js_code(); + $output = ajax_generate_script_tag($js); + $this->mark_done(); + return $output; + } + + /** + * Indicate that the link to this JavaScript file should be output in the + * <head> section of the HTML. If it too late for this request to be + * satisfied, an exception is thrown. + */ + public function in_head() { + if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_IN_HEAD) { + return; + } + if ($this->manager->is_head_done()) { + throw new coding_exception('Too late to ask for some JavaScript code to be output in <head>.'); + } + $this->when = page_requirements_manager::WHEN_IN_HEAD; + } + + /** + * Indicate that the link to this JavaScript file should be output at the top + * of the <body> section of the HTML. If it too late for this request to be + * satisfied, an exception is thrown. + */ + public function at_top_of_body() { + if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_TOP_OF_BODY) { + return; + } + if ($this->manager->is_top_of_body_done()) { + throw new coding_exception('Too late to ask for some JavaScript code to be output at the top of <body>.'); + } + $this->when = page_requirements_manager::WHEN_TOP_OF_BODY; + } +} + + +/** + * This class represents a JavaScript function that must be called from the HTML + * page. By default the call will be made at the end of the page, but you can + * chage that using the ->asap, ->in_head, etc. methods. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_js_function_call extends required_js_code { + protected $function; + protected $arguments; + + /** + * Constructor. Normally the class and its subclasses should not be created + * directly. Client code should create them via the page_requirements_manager + * method ->js_function_call(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $function the name of the JavaScritp function to call. + * Can be a compound name like 'YAHOO.util.Event.addListener'. + * @param array $arguments and array of arguments to be passed to the function. + * When generating the function call, this will be escaped using json_encode, + * so passing objects and arrays should work. + */ + public function __construct(page_requirements_manager $manager, $function, $arguments) { + parent::__construct($manager); + $this->function = $function; + $this->arguments = $arguments; + } + + public function get_js_code() { + $quotedargs = array(); + foreach ($this->arguments as $arg) { + $quotedargs[] = json_encode($arg); + } + return $this->function . '(' . implode(', ', $quotedargs) . ");\n"; + } + + /** + * Indicate that this function should be called in YUI's onDomReady event. + * + * Not that this is probably not necessary most of the time. Just having the + * function call at the end of the HTML should normally be sufficient. + */ + public function on_dom_ready() { + if ($this->is_done() || $this->when < page_requirements_manager::WHEN_AT_END) { + return; + } + $this->manager->yui_lib('event'); + $this->when = page_requirements_manager::WHEN_ON_DOM_READY; + } +} + + +/** + * This class represents some data from PHP that needs to be made available in a + * global JavaScript variable. By default the data will be output at the end of + * the page, but you can chage that using the ->asap, ->in_head, etc. methods. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_data_for_js extends required_js_code { + protected $variable; + protected $data; + + /** + * Constructor. Normally the class and its subclasses should not be created + * directly. Client code should create them via the page_requirements_manager + * method ->data_for_js(...). + * + * @param page_requirements_manager $manager the page_requirements_manager we are associated with. + * @param string $variable the the name of the JavaScript variable to assign the data to. + * Will probably work if you use a compound name like 'mybuttons.button[1]', but this + * should be considered an experimental feature. + * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode, + * so passing objects and arrays should work. + */ + public function __construct(page_requirements_manager $manager, $variable, $data) { + parent::__construct($manager); + $this->variable = $variable; + $this->data = json_encode($data); + // json_encode immediately, so that if $data is an object (and therefore was + // passed in by reference) we get the data at the time the call was made, and + // not whatever the data happened to be when this is output. + } + + public function get_js_code() { + $prefix = 'var '; + if (strpos($this->variable, '.') || strpos($this->variable, '[')) { + $prefix = ''; + } + return $prefix . $this->variable . ' = ' . $this->data . ";\n"; + } +} + + +/** + * Generate a script tag containing the the specified code. + * + * @param string $js the JavaScript code + * @return string HTML, the code wrapped in \n"; +} + + +/** + * Given the name of a YUI library, return a list of the .js and .css files that + * it requries. + * + * This method takes note of the $CFG->useexternalyui setting. + * + * If $CFG->debug is set to DEBUG_DEVELOPER then this method will return links to + * the -debug version of the YUI files, otherwise it will return links to the -min versions. + * + * @param string $libname the name of a YUI library, for example 'autocomplete'. + * @return array with two elementes. The first is an array of the JavaScript URLs + * that must be loaded to make this library work, in the order they should be + * loaded. The second element is a (possibly empty) list of CSS files that + * need to be loaded. + */ +function ajax_resolve_yui_lib($libname) { + global $CFG; + + // Note, we always use yahoo-dom-event, even if we are only asked for part of it. + // because another part of the code may later ask for other bits. It is easier, and + // not very inefficient, just to always use (and get browsers to cache) the combined file. + static $translatelist = array( + 'yahoo' => 'yahoo-dom-event', + 'animation' => array('yahoo-dom-event', 'animation'), + 'autocomplete' => array( + 'js' => array('yahoo-dom-event', 'datasource', 'autocomplete'), + 'css' => array('autocomplete/assets/skins/sam/autocomplete.css')), + 'button' => array( + 'js' => array('yahoo-dom-event', 'element', 'button'), + 'css' => array('button/assets/skins/sam/button.css')), + 'calendar' => array( + 'js' => array('yahoo-dom-event', 'calendar'), + 'css' => array('calendar/assets/skins/sam/calendar.css')), + 'carousel' => array( + 'js' => array('yahoo-dom-event', 'element', 'carousel'), + 'css' => array('carousel/assets/skins/sam/carousel.css')), + 'charts' => array('yahoo-dom-event', 'element', 'datasource', 'json', 'charts'), + 'colorpicker' => array( + 'js' => array('utilities', 'slider', 'colorpicker'), + 'css' => array('colorpicker/assets/skins/sam/colorpicker.css')), + 'connection' => array('yahoo-dom-event', 'connection'), + 'container' => array( + 'js' => array('yahoo-dom-event', 'container'), + 'css' => array('container/assets/skins/sam/container.css')), + 'cookie' => array('yahoo-dom-event', 'cookie'), + 'datasource' => array('yahoo-dom-event', 'datasource'), + 'datatable' => array( + 'js' => array('yahoo-dom-event', 'element', 'datasource', 'datatable'), + 'css' => array('datatable/assets/skins/sam/datatable.css')), + 'dom' => 'yahoo-dom-event', + 'dom-event' => 'yahoo-dom-event', + 'dragdrop' => array('yahoo-dom-event', 'dragdrop'), + 'editor' => array( + 'js' => array('yahoo-dom-event', 'element', 'container', 'menu', 'button', 'editor'), + 'css' => array('assets/skins/sam/skin.css')), + 'element' => array('yahoo-dom-event', 'element'), + 'event' => 'yahoo-dom-event', + 'get' => array('yahoo-dom-event', 'get'), + 'history' => array('yahoo-dom-event', 'history'), + 'imagecropper' => array( + 'js' => array('yahoo-dom-event', 'dragdrop', 'element', 'resize', 'imagecropper'), + 'css' => array('assets/skins/sam/resize.css', 'assets/skins/sam/imagecropper.css')), + 'imageloader' => array('yahoo-dom-event', 'imageloader'), + 'json' => array('yahoo-dom-event', 'json/json'), + 'layout' => array( + 'js' => array('yahoo-dom-event', 'dragdrop', 'element', 'layout'), + 'css' => array('reset-fonts-grids/reset-fonts-grids.css', 'assets/skins/sam/layout.css')), + 'logger' => array( + 'js' => array('yahoo-dom-event', 'logger'), + 'css' => array('logger/assets/skins/sam/logger.css')), + 'menu' => array( + 'js' => array('yahoo-dom-event', 'container', 'menu'), + 'css' => array('menu/assets/skins/sam/menu.css')), + 'paginator' => array( + 'js' => array('yahoo-dom-event', 'element', 'paginator'), + 'css' => array('paginator/assets/skins/sam/paginator.css')), + 'profiler' => array('yahoo-dom-event', 'profiler'), + 'profilerviewer' => array('yuiloader-dom-event', 'element', 'profiler', 'profilerviewer'), + 'resize' => array( + 'js' => array('yahoo-dom-event', 'dragdrop', 'element', 'resize'), + 'css' => array('assets/skins/sam/resize.css')), + 'selector' => array('yahoo-dom-event', 'selector'), + 'simpleeditor' => array( + 'js' => array('yahoo-dom-event', 'element', 'container', 'simpleeditor'), + 'css' => array('assets/skins/sam/skin.css')), + 'slider' => array('yahoo-dom-event', 'gragdrop', 'slider'), + 'stylesheet' => array('yahoo-dom-event', 'stylesheet'), + 'tabview' => array( + 'js' => array('yahoo-dom-event', 'element', 'tabview'), + 'css' => array('assets/skins/sam/skin.css')), + 'treeview' => array( + 'js' => array('yahoo-dom-event', 'treeviewed'), + 'css' => array('treeview/assets/skins/sam/treeview.css')), + 'uploader' => array('yahoo-dom-event', 'element', 'uploader'), + 'utilities' => array('yahoo-dom-event', 'connection', 'animation', 'dragdrop', 'element', 'get'), + 'yuiloader' => 'yuiloader', + 'yuitest' => array( + 'js' => array('yahoo-dom-event', 'logger', 'yuitest'), + 'css' => array('logger/assets/logger.css', 'yuitest/assets/testlogger.css')), + ); + if (!isset($translatelist[$libname])) { + throw new coding_exception('Unknown YUI library ' . $libname); + } + + $data = $translatelist[$libname]; + if (!is_array($data)) { + $jsnames = array($data); + $cssfiles = array(); + } else if (isset($data['js']) && isset($data['css'])) { + $jsnames = $data['js']; + $cssfiles = $data['css']; + } else { + $jsnames = $data; + $cssfiles = array(); + } + + $debugging = debugging('', DEBUG_DEVELOPER); + if ($debugging) { + $suffix = '-debug.js'; + } else { + $suffix = '-min.js'; + } + $libpath = $CFG->httpswwwroot . '/lib/yui/'; + + $externalyui = !empty($CFG->useexternalyui); + if ($externalyui) { + include($CFG->libdir.'/yui/version.php'); // Sets $yuiversion. + $libpath = 'http://yui.yahooapis.com/' . $yuiversion . '/build/'; + } + + $jsurls = array(); + foreach ($jsnames as $js) { + if ($js == 'yahoo-dom-event') { + if ($debugging) { + $jsurls[] = $libpath . 'yahoo/yahoo' . $suffix; + $jsurls[] = $libpath . 'dom/dom' . $suffix; + $jsurls[] = $libpath . 'event/event' . $suffix; + } else { + $jsurls[] = $jsurls[] = $libpath . $js . '/' . $js . '.js'; + } + } else { + $jsurls[] = $libpath . $js . '/' . $js . $suffix; + } + } + + $cssurls = array(); + foreach ($cssfiles as $css) { + $cssurls[] = $libpath . $css; + } + + return array($jsurls, $cssurls); +} /** * Get the path to a JavaScript library. diff --git a/lib/ajax/simpletest/testajaxlib.php b/lib/ajax/simpletest/testajaxlib.php index 6052d87b62..003e19b7ac 100644 --- a/lib/ajax/simpletest/testajaxlib.php +++ b/lib/ajax/simpletest/testajaxlib.php @@ -1,48 +1,403 @@ -. + /** * Unit tests for (some of) ../ajaxlib.php. * - * @license http://www.gnu.org/copyleft/gpl.html GNU Public License - * @package moodlecore + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ if (!defined('MOODLE_INTERNAL')) { die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page } - require_once($CFG->libdir . '/ajax/ajaxlib.php'); + +/** + * Helper class, adds some useful stuff to UnitTestCase that the other test cases + * classes in this file can benefit from. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class ajaxlib_unit_test_base extends UnitTestCase { + protected $requires; + + public function setUp() { + parent::setUp(); + $this->requires = new page_requirements_manager(); + } + + public function tearDown() { + $this->requires = null; + parent::tearDown(); + } + + public function assertContains($actual, $expectedsubstring) { + $this->assertNotIdentical(strpos($actual, (string) $expectedsubstring), false, + "[$actual] does not containg the substring [$expectedsubstring]."); + } +} + + +/** + * Unit tests for the requriement_base base class. Don't be confused by the + * fact we are using a specific subclass to test with. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class requriement_base_test extends ajaxlib_unit_test_base { + protected $classname = 'required_css'; + + public function test_not_done_initially() { + $requirement = new $this->classname($this->requires, ''); + $this->assertFalse($requirement->is_done()); + } + + public function test_done_when_marked() { + $requirement = new $this->classname($this->requires, ''); + $requirement->mark_done(); + $this->assertTrue($requirement->is_done()); + } +} + + +/** + * Unit tests for the required_css class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_css_test extends ajaxlib_unit_test_base { + protected $cssurl = 'http://example.com/styles.css'; + + public function test_when() { + $requirement = new required_css($this->requires, $this->cssurl); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD); + } + + public function test_round_trip_url_to_html() { + $requirement = new required_css($this->requires, $this->cssurl); + $html = $requirement->get_html(); + $this->assertContains($html, $this->cssurl); + $this->assertContains($html, 'assertContains($html, 'type="text/css"'); + } +} + + +/** + * Unit tests for the required_skip_link class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_skip_link_test extends ajaxlib_unit_test_base { + protected $target = 'skiptarget'; + protected $linktext = 'Skip to link text'; + + public function test_when() { + $requirement = new required_skip_link($this->requires, $this->target, $this->linktext); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_TOP_OF_BODY); + } + + public function test_round_trip_to_html() { + $requirement = new required_skip_link($this->requires, $this->target, $this->linktext); + $html = $requirement->get_html(); + $this->assertContains($html, '>' . $this->linktext . ''); + $this->assertContains($html, 'assertContains($html, 'class="skip"'); + $this->assertContains($html, 'href="#' . $this->target . '"'); + } +} + + +/** + * Unit tests for the required_js_code class. Once again we are tesing the + * behaviour of an abstract class by creating instances one particular subclass. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_js_code_test extends ajaxlib_unit_test_base { + protected $classname = 'required_data_for_js'; + + public function test_when() { + $requirement = new $this->classname($this->requires, '', ''); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_AT_END); + } + + public function test_setting_when_to_head() { + $requirement = new $this->classname($this->requires, '', ''); + $requirement->in_head(); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD); + } + + public function test_setting_when_to_top_of_body() { + $requirement = new $this->classname($this->requires, '', ''); + $requirement->at_top_of_body(); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_TOP_OF_BODY); + } + + public function test_asap_before_head_is_head() { + $requirement = new $this->classname($this->requires, '', ''); + $this->assertEqual($requirement->asap(), ''); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD); + } + + public function test_asap_after_head() { + $requirement = new $this->classname($this->requires, '', ''); + $this->requires->get_head_code(); + + $this->assertNotEqual($requirement->asap(), ''); + $this->assertTrue($requirement->is_done()); + } + + public function test_in_head_when_too_late_throws_exception() { + $requirement = new $this->classname($this->requires, '', ''); + $this->requires->get_head_code(); + + $this->expectException(); + $requirement->in_head(); + } + + public function test_in_head_when_too_late_no_exception_if_done() { + $requirement = new $this->classname($this->requires, '', ''); + $requirement->mark_done(); + $this->requires->get_head_code(); + + $requirement->in_head(); + $this->pass('No exception thrown as expected.'); + } + + public function test_body_top_when_too_late_throws_exception() { + $requirement = new $this->classname($this->requires, '', ''); + $this->requires->get_top_of_body_code(); + + $this->expectException(); + $requirement->at_top_of_body(); + } + + public function test_body_top_when_too_late_no_exception_if_done() { + $requirement = new $this->classname($this->requires, '', ''); + $requirement->mark_done(); + $this->requires->get_top_of_body_code(); + + $requirement->at_top_of_body(); + $this->pass('No exception thrown as expected.'); + } + + public function test_body_top_does_not_override_in_head() { + $requirement = new $this->classname($this->requires, '', ''); + + $requirement->in_head(); + $requirement->at_top_of_body(); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD); + } +} + + +/** + * Unit tests for the required_js_function_call class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_js_function_call_test extends ajaxlib_unit_test_base { + protected $function = 'object.method'; + protected $params = array('arg1', 2); + + public function test_round_trip_to_js_code() { + $requirement = new required_js_function_call($this->requires, $this->function, $this->params); + $js = $requirement->get_js_code(); + $this->assertContains($js, $this->function . '('); + $this->assertContains($js, $this->params[0]); + $this->assertContains($js, $this->params[1]); + } + + public function test_setting_when_on_dom_ready() { + $requirement = new required_js_function_call($this->requires, $this->function, $this->params); + $requirement->on_dom_ready(); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_ON_DOM_READY); + } + + public function test_body_top_does_not_override_earlier_value() { + $requirement = new required_js_function_call($this->requires, $this->function, $this->params); + $requirement->at_top_of_body(); + $requirement->on_dom_ready(); + $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_TOP_OF_BODY); + } +} + + +/** + * Unit tests for the required_data_for_js class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class required_data_for_js_test extends ajaxlib_unit_test_base { + public function test_round_trip_to_js_code() { + $requirement = new required_data_for_js($this->requires, 'varname', 'Value'); + $js = $requirement->get_js_code(); + $this->assertContains($js, 'var varname ='); + $this->assertContains($js, 'Value'); + } + + public function test_no_var_for_complex_varname_1() { + $requirement = new required_data_for_js($this->requires, 'obj.field', 'Value'); + $js = $requirement->get_js_code(); + $this->assertPattern('/^obj\\.field =/', $js); + $this->assertContains($js, 'Value'); + } + + public function test_no_var_for_complex_varname_2() { + $requirement = new required_data_for_js($this->requires, 'arry[0]', 'Value'); + $js = $requirement->get_js_code(); + $this->assertPattern('/^arry\\[0\\] =/', $js); + $this->assertContains($js, 'Value'); + } +} + + +/** + * Unit tests for the page_requirements_manager class. + * + * @copyright 2009 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class page_requirements_manager_test extends ajaxlib_unit_test_base { + + public function test_outputting_head_marks_it_done() { + $this->requires->get_head_code(); + $this->assertTrue($this->requires->is_head_done()); + } + + public function test_outputting_body_top_marks_it_done() { + $this->requires->get_top_of_body_code(); + $this->assertTrue($this->requires->is_top_of_body_done()); + } + + public function test_requiring_css() { + global $CFG; + $cssfile = 'theme/standard/styles_layout.css'; // Just needs to be a CSS file that exists. + $this->requires->css($cssfile); + + $html = $this->requires->get_head_code(); + $this->assertContains($html, $CFG->httpswwwroot . '/' . $cssfile); + } + + public function test_nonexistant_css_throws_exception() { + $cssfile = 'css/file/that/does/not/exist.css'; + + $this->expectException(); + $this->requires->css($cssfile); + } + + public function test_requiring_js() { + global $CFG; + $jsfile = 'lib/javascript-static.js'; // Just needs to be a JS file that exists. + $this->requires->js($jsfile); + + $html = $this->requires->get_end_code(); + $this->assertContains($html, $CFG->httpswwwroot . '/' . $jsfile); + } + + public function test_nonexistant_js_throws_exception() { + $cssfile = 'js/file/that/does/not/exist.js'; + + $this->expectException(); + $this->requires->js($cssfile); + } + + public function test_requiring_skip_link() { + $this->requires->skip_link_to('target', 'Link text'); + + $html = $this->requires->get_top_of_body_code(); + $this->assertContains($html, 'target'); + $this->assertContains($html, 'Link text'); + } + + public function test_requiring_js_function_call() { + $this->requires->js_function_call('fn'); + + $html = $this->requires->get_end_code(); + $this->assertContains($html, 'fn()'); + } + + public function test_requiring_string_for_js() { + $this->requires->string_for_js('course', 'moodle'); + + $html = $this->requires->get_end_code(); + $this->assertContains($html, 'mstr'); + $this->assertContains($html, 'course'); + $this->assertContains($html, 'moodle'); + } + + public function test_repeat_string_different_a_throws_exception() { + $this->requires->string_for_js('added', 'moodle', 'this'); + $this->expectException(); + $this->requires->string_for_js('added', 'moodle', 'that'); + } + + public function test_repeat_string_same_a_is_ok() { + $this->requires->string_for_js('added', 'moodle', 'same$a'); + $this->requires->string_for_js('added', 'moodle', 'same$a'); + $this->pass('No exception thrown as expected.'); + } + + public function test_requiring_data_for_js() { + $this->requires->data_for_js('varname', 'Value')->at_top_of_body(); + + $html = $this->requires->get_top_of_body_code(); + $this->assertContains($html, 'varname'); + $this->assertContains($html, 'Value'); + } + + public function test_requiring_js_function_call_on_dom_ready() { + $this->requires->js_function_call('fn')->on_dom_ready(); + + $html = $this->requires->get_end_code(); + $this->assertPattern('/assertContains($html, 'YAHOO.util.Event.onDOMReady'); + $this->assertContains($html, 'fn()'); + } +} + + /** - * Unit tests of mathslib wrapper and underlying EvalMath library. + * Unit tests for ../ajaxlib.php functions. * - * @author Petr Skoda (skodak) - * @version $Id$ + * @copyright 2008 Tim Hunt + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class ajax_test extends UnitTestCase { +class ajax_test extends ajaxlib_unit_test_base { + + function test_ajax_generate_script_tag() { + $html = ajax_generate_script_tag(''); + $this->assertContains($html, ''); + } + function test_ajax_get_lib() { global $CFG; $olddebug = $CFG->debug; diff --git a/lib/pagelib.php b/lib/pagelib.php index 4bcc47ba6e..5f62bafaa6 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -97,6 +97,8 @@ class moodle_page { protected $_blocks = null; + protected $_requires = null; + protected $_blockseditingcap = 'moodle/site:manageblocks'; protected $_othereditingcaps = array(); @@ -291,6 +293,19 @@ class moodle_page { return $this->_blocks; } + /** + * Please do not call this method directly, use the ->blocks syntax. @see __get(). + * @return blocks_manager the blocks manager object for this page. + */ + public function get_requires() { + global $CFG; + if (is_null($this->_requires)) { + $this->_requires = new page_requirements_manager(); + setup_core_javascript($this->_requires); + } + return $this->_requires; + } + /** * PHP overloading magic to make the $PAGE->course syntax work by redirecting * it to the corresponding $PAGE->get_course() method if there is one, and -- 2.39.5