├── .gitignore ├── README.md ├── composer.json ├── modman ├── screenshot.png ├── src ├── app │ ├── code │ │ └── community │ │ │ └── Treynolds │ │ │ └── Qconfig │ │ │ ├── Block │ │ │ └── Adminhtml │ │ │ │ ├── Qsearch.php │ │ │ │ └── System │ │ │ │ └── Config │ │ │ │ └── Edit.php │ │ │ ├── Helper │ │ │ └── Data.php │ │ │ ├── controllers │ │ │ └── Adminhtml │ │ │ │ └── QconfigController.php │ │ │ └── etc │ │ │ └── config.xml │ ├── design │ │ └── adminhtml │ │ │ └── default │ │ │ └── default │ │ │ ├── layout │ │ │ └── treynolds │ │ │ │ └── qconfig.xml │ │ │ └── template │ │ │ └── treynolds │ │ │ └── qconfig │ │ │ └── qsearch.phtml │ └── etc │ │ └── modules │ │ └── Treynolds_Qconfig.xml ├── js │ └── treynolds │ │ └── qconfig.js └── skin │ └── adminhtml │ └── base │ └── default │ └── treynolds │ ├── ajax-loader.gif │ └── qconfig.css └── test └── js ├── prototype.js ├── qunit-1.10.0.css ├── qunit-1.10.0.js └── tests.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | src/app/code/core/ 3 | src/app/code/local/ 4 | src/app/code/community/Phoenix/ 5 | src/app/locale/ 6 | src/app/design/frontend/ 7 | 8 | src/app/etc/ 9 | src/downloader/ 10 | src/errors/ 11 | src/includes/ 12 | 13 | src/lib/ 14 | src/media/ 15 | src/pkginfo/ 16 | src/shell/ 17 | src/skin/frontend/ 18 | src/skin/adminhtml/default/ 19 | src/var/ 20 | src/*.php 21 | src/*.sh 22 | src/*.ico 23 | src/*.html 24 | src/.htaccess 25 | src/.htaccess.sample 26 | src/*.sample 27 | src/mage 28 | src/*.txt 29 | src/app/Mage.php 30 | src/app/.htaccess 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Open Source Initiative OSI - The MIT License (MIT):Licensing 4 | * 5 | * The MIT License (MIT) 6 | * Copyright (c) 2012 Tim Reynolds 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | * 12 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | */ 14 | 15 | 16 | Magento Quick Config Module 17 | =========================== 18 | 19 | I created this module to make working in the System -> Configuration area of the Magento Admin easier for my clients. With qsearch you get a text box that will search the labels and values of fields for any that match your input. Areas that don't match will be shaded out, allowing you to quickly and accurately navigate the configuration area. Simple, but effective. 20 | 21 | ![Screen Shot of QConfig](https://raw.github.com/tim-reynolds/magento-qconfig/master/screenshot.png) 22 | 23 | [Youtube Video showing how it works!](http://www.youtube.com/watch?v=t683rxYvEYg) 24 | 25 | Installation 26 | ------------ 27 | 28 | Clone the repository down to your computer. Copy the contents of the src/ directory into your Magento root directory. Then go into the admin and clear the config and layout caches. 29 | 30 | This has been tested in Community 1.7 and Enterprise 1.12. If you have any issues please reach out, though as stated in the license this comes with no warranty. Please test in development before pushing to production! 31 | 32 | TODO 33 | ---- 34 | 35 | There is no support yet for single/multi-select inputs that have a source model. I have worked on a few attempts at this, however I don't yet have a solution I am comfortable with. If you search for "enabled" you won't find much, as the actual value is "1" in the data. Additionally, searches for the Country/Region/Locale text names won't work, but if you search for the short-code it will (en_us vs United States). 36 | 37 | Shameless Plug 38 | -------------- 39 | 40 | I hope you enjoy this module. I have a few other modules I want to give back to the community. If you enjoy this, and need any help on a commercial project please don't hesitate to reach out. I can be contacted at Reynolds.TimJ@gmail.com or on Twitter @razialx. 41 | 42 | Motivation and Thanks 43 | --------------------- 44 | 45 | As the Magento community has been amazing to me, I decided to give this back as some small token of appreciation. I have long wanted to write this, but was always busy. The final motivation came when Alan Storm (@alanstorm, http://alanstorm.com) released an excellent module for quickly navigating the Admin menu with your keyboard. You should also buy his e-book on Magento Layouts. I also want to thank other great community members (and forgive me, I will surely forget many): @VinaiKopp @fbrnc @sherrierohde @sparcksoft @kab8609 @benmarks @markshust @monocat @arush @b_ike @colinmollenhour @alistairstead @aschroder @cloudhead @zerkella @s3lf and many many more. Thanks for making this the best software community around! 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tim-reynolds/magento-qconfig", 3 | "license": "OSL-3.0", 4 | "type": "magento-module", 5 | "description": "Magento config quick search", 6 | "homepage": "https://github.com/tim-reynolds/", 7 | "require": { 8 | "magento-hackathon/magento-composer-installer": "*" 9 | }, 10 | "authors":[ 11 | { 12 | "name":"Tim Reynolds" 13 | } 14 | ] 15 | } 16 | 17 | -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | src/app/code/community/Treynolds/Qconfig app/code/community/Treynolds/Qconfig 2 | src/app/design/adminhtml/default/default/layout/treynolds app/design/adminhtml/default/default/layout/treynolds 3 | src/app/design/adminhtml/default/default/template/treynolds app/design/adminhtml/default/default/template/treynolds 4 | src/js/treynolds js/treynolds 5 | src/skin/adminhtml/base/default/treynolds skin/adminhtml/base/default/treynolds 6 | src/app/etc/modules/* app/etc/modules/ 7 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-reynolds/magento-qconfig/f810f7ba689f7a088081696944377a6eb2b80964/screenshot.png -------------------------------------------------------------------------------- /src/app/code/community/Treynolds/Qconfig/Block/Adminhtml/Qsearch.php: -------------------------------------------------------------------------------- 1 | setTemplate('treynolds/qconfig/qsearch.phtml'); 7 | } 8 | } -------------------------------------------------------------------------------- /src/app/code/community/Treynolds/Qconfig/Block/Adminhtml/System/Config/Edit.php: -------------------------------------------------------------------------------- 1 | setChild('qsearch', $this->getLayout()->createBlock('qconfig/adminhtml_qsearch')); 9 | return parent::_prepareLayout(); 10 | } 11 | public function getSaveButtonHtml() 12 | { 13 | return $this->getChildHtml('qsearch') . parent::getSaveButtonHtml(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/code/community/Treynolds/Qconfig/Helper/Data.php: -------------------------------------------------------------------------------- 1 | xpath('*[.//label[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "'.$qsearch.'") and ../'.$levelClause.'="1"]]') 15 | ,$configRoot->xpath('*[./*/*[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "'. $qsearch.'")]]') 16 | 17 | ); 18 | 19 | /* @var $node Mage_Core_Model_Config_Element */ 20 | foreach($nodes as $node){ 21 | $nav_ret[] = 'section/'. $node->getName(0); 22 | } 23 | return $nav_ret; 24 | } 25 | /** 26 | * @param $qsearch string 27 | * @param $sections Mage_Core_Model_Config_Element 28 | * @param $configRoot Varien_Simplexml_Element 29 | * @param $levelClause string 30 | * @return array 31 | */ 32 | protected function devGetNavRecords($qsearch, $sections, $configRoot, $levelClause){ 33 | $tmp_nav_counts = array(); 34 | $tmp_track = array(); 35 | $nodes = $configRoot->xpath('*/*/*[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "'. $qsearch.'")]'); 36 | foreach($nodes as $node){ 37 | $group = $node->xpath('..'); 38 | $section = $group[0]->xpath('..'); 39 | $section_name = $section[0]->getName(); 40 | $tmp_track[$section_name . '/' . $group[0]->getName() . '/' . $node->getName()] = 1; 41 | if(isset($tmp_nav_counts[$section_name])){ 42 | $tmp_nav_counts[$section_name][0]++; 43 | } 44 | else { 45 | $tmp_nav_counts[$section_name] = array(1,0); 46 | } 47 | } 48 | 49 | $nodes = $sections->xpath('*/groups//label[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "'.$qsearch.'") and ../'.$levelClause.'="1"]'); 50 | 51 | foreach($nodes as $node){ 52 | $path = array(); 53 | $parent = $node->xpath('.'); 54 | $sanity = 0; 55 | while($parent!==false && count($parent)>0 && $parent[0]->getName()!='sections' && $sanity++ < 10){ 56 | $path[] = $parent[0]->getName(); 57 | $parent = $parent[0]->xpath('./..'); 58 | } 59 | $tmp_section = false; 60 | $tmp_group = false; 61 | $tmp_field = false; 62 | 63 | /* The count is 4 when we matched a 'group' label */ 64 | if(count($path)==4){ 65 | 66 | $tmp_section = $path[3]; 67 | $tmp_group = true;//$path[3]. '/' . $path[1]; 68 | 69 | } 70 | /* The count is 6 when we match a 'field' label */ 71 | else if(count($path)==6) { 72 | $tmp_section = $path[5]; 73 | $tmp_field = $path[5] . '/' . $path[3] . '/' . $path[1]; 74 | } 75 | 76 | 77 | if($tmp_section!==false){ 78 | if($tmp_group){ 79 | if(isset($tmp_nav_counts[$tmp_section])){ 80 | $tmp_nav_counts[$tmp_section][1]++; 81 | } 82 | else { 83 | $tmp_nav_counts[$tmp_section]= array(0,1); 84 | } 85 | } 86 | 87 | if($tmp_field && !isset($tmp_track[$tmp_field])){ 88 | $tmp_track[$tmp_field] = 1; 89 | if(isset($tmp_nav_counts[$tmp_section])){ 90 | $tmp_nav_counts[$tmp_section][0]++; 91 | 92 | } 93 | else { 94 | $tmp_nav_counts[$tmp_section]= array(1,0); 95 | 96 | } 97 | } 98 | } 99 | 100 | } 101 | $nav_ret = array(); 102 | foreach($tmp_nav_counts as $section=>$counts){ 103 | $nav_ret[] = array('section/'.$section, $counts[0], $counts[1]); 104 | } 105 | 106 | 107 | return $nav_ret; 108 | } 109 | 110 | /** 111 | * @param $qsearch string 112 | * @param $current string 113 | * @param $sections Mage_Core_Model_Config_Element 114 | * @param $levelClause string 115 | * @return array 116 | */ 117 | protected function getGroupAndFieldRecordsByLabel($qsearch, $current, $sections, $levelClause){ 118 | $group_ret = array(); 119 | $field_ret = array(); 120 | $nodes = $sections->xpath($current . '/groups//label[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "'.$qsearch.'") and ../'.$levelClause.'="1"]'); 121 | 122 | foreach($nodes as $node){ 123 | $path = array(); 124 | $parent = $node->xpath('.'); 125 | $sanity = 0; 126 | while($parent[0]->getName()!=$current && $sanity++ < 10){ 127 | $path[] = $parent[0]->getName(); 128 | $parent = $parent[0]->xpath('./..'); 129 | } 130 | $path[] = $current; 131 | /* The count is 4 when we matched a 'group' label */ 132 | if(count($path)==4){ 133 | $group_ret[] = $path[3]. '_' . $path[1]; 134 | } 135 | /* The count is 6 when we match a 'field' label */ 136 | else if(count($path)==6) { 137 | $group_ret[] = $path[5]. '_' . $path[3]; 138 | $field_ret[] ='row_' . $path[5] . '_' . $path[3] . '_' . $path[1]; 139 | } 140 | 141 | } 142 | 143 | return array($group_ret, $field_ret); 144 | } 145 | 146 | /** 147 | * @param $qsearch string 148 | * @param $current string 149 | * @param $configRoot Varien_Simplexml_Element 150 | * @return array 151 | */ 152 | protected function getGroupAndFieldRecordsByValue($qsearch, $current, $configRoot){ 153 | $group_ret = array(); 154 | $field_ret = array(); 155 | 156 | $nodes = $configRoot->xpath($current . '//*[contains(translate(text(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "'. $qsearch.'")]'); 157 | foreach($nodes as $node){ 158 | $path = array(); 159 | 160 | $parent = $node->xpath('.'); 161 | $sanity = 0; 162 | while($parent[0]->getName()!=$current && $sanity++ < 10){ 163 | $path[] = $parent[0]->getName(); 164 | $parent = $parent[0]->xpath('./..'); 165 | } 166 | $path[] = $current; 167 | if(count($path)==3){ 168 | $field_ret[] = 'row_' . $path[2] . '_' . $path[1] . '_' . $path[0]; 169 | $group_ret[] = $path[2] . '_' . $path[1]; 170 | } 171 | 172 | } 173 | 174 | 175 | return array($group_ret, $field_ret); 176 | } 177 | 178 | /** 179 | * This function will load a module's system.xml file and find all fields in it. Does not 180 | * actually do string searching, just finds everything defined. 181 | * 182 | * @param $module string 183 | * @param $current string 184 | * @param $levelClause string 185 | * @return array 186 | */ 187 | protected function getModuleSpecificRecords($module, $current, $levelClause) { 188 | 189 | // $module = uc_words($module); 190 | 191 | /* @var $conf Mage_Core_Model_Config_System */ 192 | $conf = Mage::getModel('core/config_system'); 193 | $conf->load($module); 194 | /* @var $sections Mage_Core_Model_Config_Element */ 195 | 196 | $sections = $conf->getNode('sections'); 197 | if(!$sections){ 198 | return array(array(),array(),array()); 199 | } 200 | $nodes = $sections->xpath('*[.//label[../'.$levelClause.'="1"]]'); 201 | 202 | 203 | //$nodes = $sections->xpath($current . '/groups//label[../'.$levelClause.'="1"]'); 204 | $nav_ret = array(); 205 | $group_ret = array(); 206 | $field_ret = array(); 207 | /* @var $node Mage_Core_Model_Config_Element */ 208 | foreach($nodes as $node){ 209 | $nav_ret[] = 'section/'. $node->getName(0); 210 | } 211 | 212 | $nodes = $sections->xpath($current . '/groups//label[../'.$levelClause.'="1"]'); 213 | foreach($nodes as $node){ 214 | $path = array(); 215 | $parent = $node->xpath('.'); 216 | $sanity = 0; 217 | while($parent[0]->getName()!=$current && $sanity++ < 10){ 218 | $path[] = $parent[0]->getName(); 219 | $parent = $parent[0]->xpath('./..'); 220 | } 221 | $path[] = $current; 222 | /* The count is 4 when we matched a 'group' label */ 223 | if(count($path)==4){ 224 | $group_ret[] = $path[3]. '_' . $path[1]; 225 | } 226 | /* The count is 6 when we match a 'field' label */ 227 | else if(count($path)==6) { 228 | $group_ret[] = $path[5]. '_' . $path[3]; 229 | $field_ret[] ='row_' . $path[5] . '_' . $path[3] . '_' . $path[1]; 230 | } 231 | 232 | } 233 | 234 | return array($nav_ret, $group_ret, $field_ret); 235 | } 236 | 237 | /** 238 | * @param $qsearch string Query String 239 | * @param $current string The current section of config you are viewing 240 | * @param $website string The current website you are under. Can be null or empty string 241 | * @param $store string The store view you are under. Can be null or empty string 242 | * @return array with keys (nav, group, field), each of which is an array of strings 243 | */ 244 | public function getQuickSearchResults($qsearch, $current, $website, $store){ 245 | if(is_null($current)){ 246 | $current = 'general';//This is currently not needed. Parameter gets set in adminhtml/system_config_tabs:122 247 | } 248 | 249 | $qsearchTrim = trim($qsearch); 250 | $qsearch = strtolower($qsearchTrim); 251 | 252 | if(strlen($qsearch)==0){ 253 | return array('nav'=>array(),'group'=>array(), 'field'=>array()); 254 | } 255 | 256 | $qsearch = preg_replace('/("|\[|\]|\(|\))/','',$qsearch); 257 | $levelClause = $this->getLevelClause($website, $store); 258 | 259 | if(!preg_match('/^module:(.+)/', $qsearchTrim, $matches)){ 260 | /* @var $formBlock Mage_Adminhtml_Block_System_Config_Form */ 261 | $formBlock = Mage::app()->getLayout()->createBlock('adminhtml/system_config_form'); 262 | /* @var $sections Varien_Simplexml_Element */ 263 | $configRoot = $formBlock->getConfigRoot(); 264 | /* @var $sections Mage_Core_Model_Config_Element */ 265 | $sections = $this->getSections($current, $website, $store); 266 | /** 267 | * First, get the top-level nodes for the left-hand nav. 268 | */ 269 | $nav_ret = $this->getNavRecords($qsearch, $sections, $configRoot, $levelClause); 270 | 271 | 272 | /** 273 | * For finding the elements on your page we have to do things a little different 274 | * We can't combine the xpath because we are grabbing the lowest level nodes 275 | * and since the xml structure of the Config differs from the structure of the 276 | * config display xml the parsing is slightly different. 277 | * Essentially, in the config display xml there is a max depth and there are 278 | * filler tags (groups, fields). In the actual config xml there aren't fillers 279 | * and the depth can be more variable. 280 | * 281 | * This results in an array with duplicates, but that doesn't have much effect 282 | * on the front-end. 283 | */ 284 | /* Config display xml for the page you are on */ 285 | $by_label = $this->getGroupAndFieldRecordsByLabel($qsearch, $current, $sections, $levelClause); 286 | /* Next we get the actual config xml for the page you are on */ 287 | $by_value = $this->getGroupAndFieldRecordsByValue($qsearch, $current, $configRoot); 288 | $group_ret = array_merge($by_value[0], $by_label[0]); 289 | $field_ret = array_merge($by_value[1], $by_label[1]); 290 | } 291 | else { 292 | list($nav_ret, $group_ret, $field_ret) = $this->getModuleSpecificRecords($matches[1], $current, $levelClause); 293 | 294 | } 295 | /* Finally, we handle edge cases */ 296 | //TODO: Figure out how to handle edge cases 297 | 298 | 299 | 300 | return array('nav'=>$nav_ret, 'group'=>$group_ret, 'field'=>$field_ret); 301 | } 302 | 303 | /** 304 | * Translate $sections 305 | * 306 | * @param $sections 307 | */ 308 | protected function translateSections(&$sections) { 309 | $configFields = Mage::getSingleton('adminhtml/config'); 310 | foreach($sections->children() as $section) { 311 | $helperName = $configFields->getAttributeModule($section); 312 | $section->label = Mage::helper($helperName)->__((string)$section->label); 313 | 314 | foreach($section->groups->children() as $group) { 315 | $helperName = $configFields->getAttributeModule($section, $group); 316 | $group->label = Mage::helper($helperName)->__((string)$group->label); 317 | 318 | if ($group->fields) { 319 | foreach($group->fields->children() as $element) { 320 | $helperName = $configFields->getAttributeModule($section, $group, $element); 321 | $element->label = Mage::helper($helperName)->__((string)$element->label); 322 | $element->hint = (string)$element->hint ? Mage::helper($helperName)->__((string)$element->hint) : ''; 323 | } 324 | } 325 | } 326 | } 327 | } 328 | 329 | /** 330 | * @param $current string 331 | * @param $website string 332 | * @param $store string 333 | * @return Mage_Core_Model_Config_Element 334 | */ 335 | protected function getSections($current, $website, $store){ 336 | /* TODO Have a look at Mage_Adminhtml_Block_System_Config_Form::initFields 337 | there is a fieldPrefix involved which does not seem to be processed here 338 | (but also might not be used at all) 339 | */ 340 | /* @var $cache Mage_Core_Model_Cache */ 341 | $locale = Mage::app()->getLocale()->getLocaleCode(); 342 | $cache = Mage::getSingleton('core/cache'); 343 | $cache_id = 'treynolds_qcache_' . $website . '_' . $store . '_' . $locale; 344 | /* Check the cache */ 345 | /* @var $sections Mage_Core_Model_Config_Element */ 346 | $sections = null; 347 | /* @var $sections_xml string */ 348 | $sections_xml = $cache->load($cache_id); 349 | if(!$sections_xml){ 350 | /* @var $configFields Mage_Adminhtml_Model_Config */ 351 | $configFields = Mage::getSingleton('adminhtml/config'); 352 | $sections = $configFields->getSections($current); 353 | 354 | $this->translateSections($sections); 355 | $cache->save($sections->asXml(), $cache_id, array(Mage_Core_Model_Config::CACHE_TAG)); 356 | } 357 | else { 358 | $sections = new Mage_Core_Model_Config_Element($sections_xml); 359 | } 360 | 361 | return $sections; 362 | } 363 | 364 | /** 365 | * @return array where the key is a string to match qsearch 366 | * and the value is an array of xpath clauses 367 | */ 368 | protected function getNavEdgeCases(){ 369 | return array('yes'=>1, 'no'=>0, 'enabled'=>1, 'disabled'=>0); 370 | } 371 | 372 | /** 373 | * Need to check the "show_in_X" tags in system.xml files 374 | * @param $website string 375 | * @param $store string 376 | * @return string 377 | */ 378 | protected function getLevelClause($website, $store){ 379 | if(!is_null($store) && strlen($store)>0){ 380 | return 'show_in_store'; 381 | } 382 | if(!is_null($website) && strlen($website)>0){ 383 | return 'show_in_website'; 384 | } 385 | return 'show_in_default'; 386 | } 387 | } -------------------------------------------------------------------------------- /src/app/code/community/Treynolds/Qconfig/controllers/Adminhtml/QconfigController.php: -------------------------------------------------------------------------------- 1 | getRequest()->getParam('qsearch'); 6 | $_website = $this->getRequest()->getParam('website'); 7 | $_store = $this->getRequest()->getParam('store'); 8 | $_section = $this->getRequest()->getParam('section'); 9 | header('Content-Type: application/json'); 10 | echo Mage::helper('core')->jsonEncode(Mage::helper('qconfig')->getQuickSearchResults($_qsearch, $_section, $_website, $_store)); 11 | exit(); 12 | } 13 | 14 | 15 | 16 | } -------------------------------------------------------------------------------- /src/app/code/community/Treynolds/Qconfig/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0.0.1 6 | 7 | 8 | 9 | 10 | 11 | Treynolds_Qconfig_Helper 12 | 13 | 14 | 15 | 16 | Treynolds_Qconfig_Block 17 | 18 | 19 | 20 | Treynolds_Qconfig_Block_Adminhtml_System_Config_Edit 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | treynolds/qconfig.xml 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Treynolds_Qconfig_Adminhtml 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/app/design/adminhtml/default/default/layout/treynolds/qconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | treynolds/qconfig.css 7 | 8 | 9 | treynolds/qconfig.js 10 | 11 | 12 | 13 | 14 | 15 | 16 | treynolds/qconfig.css 17 | 18 | 19 | treynolds/qconfig.js 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/app/design/adminhtml/default/default/template/treynolds/qconfig/qsearch.phtml: -------------------------------------------------------------------------------- 1 | getRequest()->getParam('qsearch'); 3 | $_website = $this->getRequest()->getParam('website'); 4 | $_store = $this->getRequest()->getParam('store'); 5 | $_section = $this->getRequest()->getParam('section'); 6 | ?> 7 | 10 |
11 | 12 |
13 | __('No Matches'); ?> 14 | 15 |
16 |
17 | 18 | 22 | -------------------------------------------------------------------------------- /src/app/etc/modules/Treynolds_Qconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | community 6 | true 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/js/treynolds/qconfig.js: -------------------------------------------------------------------------------- 1 | Qconfig = Class.create(); 2 | Qconfig.prototype = { 3 | initialize:function (url, website, store, section) { 4 | this.url = url; 5 | this.website = website; 6 | this.store = store; 7 | this.section = section; 8 | this.request = null; 9 | this.timeout = null; 10 | this.timeout_delay = 400; 11 | }, 12 | onkeyup:function (box) { 13 | if (box.getValue() == '') { 14 | this.onescape(); 15 | return; 16 | } 17 | $$('.treynolds_qconfig_box').each(function (sync_box) { 18 | if (sync_box != box) { 19 | sync_box.setValue(box.getValue()); 20 | } 21 | 22 | }); 23 | if (this.timeout != null) { 24 | clearTimeout(this.timeout); 25 | } 26 | this.timeout = setTimeout(this.ontimeout.bind(this), this.timeout_delay); 27 | this.updateclearbutton(); 28 | }, 29 | ontimeout:function () { 30 | this.timeout = null; 31 | 32 | if (this.request != null) { 33 | this.request.abort(); 34 | } 35 | var query_string = $('treynolds_qconfig_box').getValue().strip(); 36 | if (query_string.length == 0) { 37 | this.clear_searching(); 38 | return; 39 | } 40 | $$('.treynolds_qconfig_loading').each( 41 | function(elm){ 42 | elm.addClassName('treynolds_loading'); 43 | } 44 | ); 45 | new Ajax.Request(this.url, { 46 | method:'get', 47 | loaderArea:false, 48 | parameters:{ 49 | qsearch:query_string, 50 | section:this.section, 51 | website:this.website, 52 | store:this.store 53 | }, 54 | onSuccess:this.onsuccess.bind(this) 55 | 56 | } 57 | ); 58 | }, 59 | onsuccess:function (transport) { 60 | this.handle_success(transport.responseJSON); 61 | }, 62 | handle_success:function (data) { 63 | this.clear_searching(); 64 | $$('#system_config_tabs, .entry-edit').each( 65 | function (elm) { 66 | elm.addClassName('treynolds_searching'); 67 | } 68 | ); 69 | 70 | if(data.nav.length == 0 && data.group.length == 0 && data.field.length == 0){ 71 | this.handle_no_results(); 72 | return; 73 | } 74 | 75 | for (var i = 0; i < data.nav.length; i++) { 76 | $$('#system_config_tabs a[href*="' + data.nav[i] + '"]').each( 77 | function (elm) { 78 | elm.up().addClassName('treynolds_active'); 79 | } 80 | ); 81 | } 82 | var tmp = null; 83 | if (data.group.length > 0) { 84 | for (i = 0; i < data.group.length; i++) { 85 | tmp = $(data.group[i] + '-head'); 86 | if (tmp != null) { 87 | tmp.up().addClassName('treynolds_active'); 88 | } 89 | } 90 | } 91 | if (data.field.length > 0) { 92 | for (i = 0; i < data.field.length; i++) { 93 | tmp = $(data.field[i]); 94 | if (tmp != null) { 95 | tmp.addClassName('treynolds_active'); 96 | } 97 | } 98 | } 99 | 100 | $$('.treynolds_active').each(function (t) { 101 | 102 | if (t.previous() != null && t.previous().hasClassName('treynolds_active')) { 103 | t.addClassName('treynolds_bottom'); 104 | } 105 | if (t.next() != null && t.next().hasClassName('treynolds_active')) { 106 | t.addClassName('treynolds_top'); 107 | } 108 | if(t.hasClassName('entry-edit-head')){ 109 | var count = t.next().next().select('.form-list .treynolds_active').length ; 110 | var span = new Element('span', {'class':'treynolds_qconfig_field_count'}).update(count + ' Field'+(count==1?' Matches':'s Match')); 111 | t.select('a')[0].insert(span); 112 | } 113 | }); 114 | }, 115 | onescape:function(){ 116 | //Don't want a request coming back after we clear. 117 | if(this.timeout != null){ 118 | clearTimeout(this.timeout); 119 | this.timeout = null; 120 | } 121 | $$('.treynolds_qconfig_box').each(function(box){ 122 | box.setValue(''); 123 | }); 124 | this.clear_searching(); 125 | this.updateclearbutton(); 126 | }, 127 | handle_no_results:function(){ 128 | $$('.treynolds_qconfig_box_wrap').each(function(noresults){ 129 | noresults.addClassName('no_results'); 130 | }); 131 | }, 132 | clear_searching:function () { 133 | $$('.treynolds_loading').each( 134 | function (elm){ 135 | elm.removeClassName('treynolds_loading'); 136 | } 137 | ); 138 | $$('.no_results').each( 139 | function (elm){ 140 | elm.removeClassName('no_results'); 141 | } 142 | ); 143 | $$('.treynolds_active').each( 144 | function (elm) { 145 | elm.removeClassName('treynolds_active'); 146 | elm.removeClassName('treynolds_top'); 147 | elm.removeClassName('treynolds_bottom'); 148 | } 149 | ); 150 | $$('.treynolds_searching').each( 151 | function (elm) { 152 | elm.removeClassName('treynolds_searching'); 153 | } 154 | ); 155 | $$('.treynolds_qconfig_field_count, b.treynolds_nav_count').each( 156 | function(elm){ 157 | elm.remove(); 158 | } 159 | ); 160 | }, 161 | updateclearbutton: function() { 162 | if ($('treynolds_qconfig_box').getValue() == '') { 163 | $('treynolds_qconfig_clear').hide(); 164 | } else { 165 | $('treynolds_qconfig_clear').show(); 166 | } 167 | } 168 | }; 169 | -------------------------------------------------------------------------------- /src/skin/adminhtml/base/default/treynolds/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tim-reynolds/magento-qconfig/f810f7ba689f7a088081696944377a6eb2b80964/src/skin/adminhtml/base/default/treynolds/ajax-loader.gif -------------------------------------------------------------------------------- /src/skin/adminhtml/base/default/treynolds/qconfig.css: -------------------------------------------------------------------------------- 1 | .treynolds_qconfig_clear, .treynolds_qconfig_clear:hover { 2 | display: inline-block; 3 | position: relative; 4 | width: 20px; 5 | height: 20px; 6 | 7 | margin-left: -23px; 8 | background: url(../../../default/default/images/cancel_icon.gif) center center no-repeat; 9 | text-decoration: none; 10 | } 11 | 12 | .treynolds_qconfig_loading { 13 | background: url(ajax-loader.gif) top left no-repeat; 14 | display: none; 15 | position: absolute; 16 | width: 32px; 17 | height: 32px; 18 | top: -6px; 19 | left: -6px; 20 | } 21 | .treynolds_qconfig_loading.treynolds_loading { 22 | display: block; 23 | } 24 | .treynolds_qconfig_box_wrap { 25 | display: inline-block; 26 | /* IE7 Fix */ 27 | zoom: 1; 28 | *display: inline; 29 | position: relative; 30 | } 31 | .treynolds_qconfig_box_wrap .treynolds_qconfig_no_results { 32 | /*background-color: #6F8992;*/ 33 | background: url(../../../default/default/images/nav1_active.gif) center center no-repeat; 34 | color: #fff; 35 | font-weight: bold; 36 | text-align: center; 37 | position: absolute; 38 | top: 100%; 39 | left: 0; 40 | right: 0; 41 | border-bottom-left-radius: 6px; 42 | border-bottom-right-radius: 6px; 43 | height: 0; 44 | opacity: 0; 45 | /* IE8 Fix */ 46 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; 47 | /* IE7 Fix */ 48 | filter: alpha(opacity=0); 49 | -webkit-transition: all 0.5s ease; 50 | -moz-transition: all 0.5s ease; 51 | -ms-transition: all 0.5s ease; 52 | -o-transition: all 0.5s ease; 53 | transition: all 0.5s ease; 54 | 55 | } 56 | .treynolds_qconfig_box_wrap.no_results .treynolds_qconfig_no_results{ 57 | opacity: 1.0; 58 | /* IE8 Fix */ 59 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; 60 | /* IE7 Fix */ 61 | filter: alpha(opacity=100); 62 | height: 20px; 63 | -webkit-transition: all 0.5s ease; 64 | -moz-transition: all 0.5s ease; 65 | -ms-transition: all 0.5s ease; 66 | -o-transition: all 0.5s ease; 67 | transition: all 0.5s ease; 68 | } 69 | 70 | #treynolds_qconfig_box{ 71 | padding-right: 20px; 72 | } 73 | .treynolds_qconfig_field_count { 74 | float: right; 75 | text-align: center; 76 | padding: 0 7px; 77 | margin-right: 25px; 78 | background: url(../../../default/default/images/nav1_active.gif) center center no-repeat; 79 | border-radius: 8px; 80 | 81 | } 82 | 83 | #system_config_tabs.treynolds_searching dd.treynolds_active span b { 84 | float: right; 85 | color: #fff; 86 | background: url(../../../default/default/images/nav1_active.gif) center center no-repeat; 87 | padding: 0 4px; 88 | border-radius: 8px; 89 | 90 | } 91 | 92 | #system_config_tabs.treynolds_searching dd a , 93 | #system_config_tabs.treynolds_searching dd a:hover { 94 | background: #929292 none; 95 | 96 | } 97 | #system_config_tabs.treynolds_searching dd a span , 98 | #system_config_tabs.treynolds_searching dd a:hover span { 99 | cursor: no-drop; 100 | background: #929292 none; 101 | } 102 | #system_config_tabs.treynolds_searching dd a.active span , 103 | #system_config_tabs.treynolds_searching dd a.active:hover span { 104 | cursor: no-drop; 105 | background: #929292 none; 106 | } 107 | #system_config_tabs.treynolds_searching dd a.paypal-section:hover, 108 | #system_config_tabs.treynolds_searching dd a.paypal-section { 109 | padding: 0; 110 | } 111 | #system_config_tabs.treynolds_searching dd a.paypal-section:hover span , 112 | #system_config_tabs.treynolds_searching dd a.paypal-section span { 113 | height: auto; 114 | overflow: visible; 115 | width: auto; 116 | padding:.3em 0.5em .28em 1.5em; 117 | } 118 | #system_config_tabs.treynolds_searching { 119 | background: #929292 none !important; 120 | } 121 | 122 | #system_config_tabs.treynolds_searching .treynolds_active span { 123 | cursor: pointer !important; 124 | 125 | background:url(../../../default/default/images/tabs_link_bg.gif) repeat-y 100% #E7EFEF !important; 126 | font-weight: bold; 127 | border-radius: 12px; 128 | } 129 | #system_config_tabs.treynolds_searching .treynolds_active a.active { 130 | border-bottom: 0; 131 | } 132 | #system_config_tabs.treynolds_searching .treynolds_active a.active span { 133 | background-color: #fff !important; 134 | } 135 | #system_config_tabs.treynolds_searching .treynolds_active a:hover span { 136 | background-color: #d8e6e6 !important; 137 | } 138 | 139 | #system_config_tabs.treynolds_searching .treynolds_active.treynolds_top span { 140 | border-bottom-right-radius: 0 !important; 141 | border-bottom-left-radius: 0 !important; 142 | } 143 | #system_config_tabs.treynolds_searching .treynolds_active.treynolds_bottom span { 144 | border-top-right-radius: 0 !important; 145 | border-top-left-radius: 0 !important; 146 | } 147 | 148 | .entry-edit.treynolds_searching .entry-edit-head , 149 | .entry-edit.treynolds_searching .entry-edit-head a { 150 | background-color: #596f77; 151 | color: #cbcbcb; 152 | cursor: no-drop; 153 | font-weight: normal; 154 | } 155 | .entry-edit.treynolds_searching .entry-edit-head.treynolds_active { 156 | /*background-color: #7c98a2;*/ 157 | padding: 2px 10px 2px 5px; 158 | cursor: pointer !important; 159 | color: #fff; 160 | } 161 | .entry-edit.treynolds_searching .entry-edit-head.treynolds_active a{ 162 | border-radius: 9px; 163 | padding: 0 0 0 5px; 164 | background-color: #7c98a2; 165 | font-weight: bold; 166 | color: #fff; 167 | cursor: pointer !important; 168 | } 169 | .entry-edit.treynolds_searching .entry-edit-head.disabled a , 170 | .entry-edit.treynolds_searching .entry-edit-head.disabled.treynolds_active a { 171 | background-color: #c6cbc9; 172 | } 173 | 174 | .entry-edit.treynolds_searching fieldset , 175 | .entry-edit.treynolds_searching input , 176 | .entry-edit.treynolds_searching textarea , 177 | .entry-edit.treynolds_searching select { 178 | background-color: #a7a7a7; 179 | } 180 | .entry-edit.treynolds_searching tr { 181 | 182 | } 183 | 184 | .entry-edit.treynolds_searching tr.treynolds_active td { 185 | background-color: #fafafa !important; 186 | 187 | } 188 | .entry-edit.treynolds_searching tr.treynolds_active input , 189 | .entry-edit.treynolds_searching tr.treynolds_active select , 190 | .entry-edit.treynolds_searching tr.treynolds_active textarea { 191 | background-color: #fff !important; 192 | } 193 | 194 | .entry-edit.treynolds_searching tr.treynolds_active td:first-child{ 195 | border-top-left-radius: 12px; 196 | border-bottom-left-radius: 12px; 197 | } 198 | .entry-edit.treynolds_searching tr.treynolds_active td:last-child{ 199 | border-top-right-radius: 12px; 200 | border-bottom-right-radius: 12px; 201 | min-width: 12px; 202 | } 203 | .entry-edit.treynolds_searching tr.treynolds_active.treynolds_top td:first-child{ 204 | border-bottom-left-radius: 0!important; 205 | } 206 | .entry-edit.treynolds_searching tr.treynolds_active.treynolds_top td:last-child{ 207 | border-bottom-right-radius: 0!important; 208 | } 209 | .entry-edit.treynolds_searching tr.treynolds_active.treynolds_bottom td:first-child{ 210 | border-top-left-radius: 0!important; 211 | } 212 | .entry-edit.treynolds_searching tr.treynolds_active.treynolds_bottom td:last-child{ 213 | border-top-right-radius: 0!important; 214 | } -------------------------------------------------------------------------------- /test/js/qunit-1.10.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests ol { 115 | margin-top: 0.5em; 116 | padding: 0.5em; 117 | 118 | background-color: #fff; 119 | 120 | border-radius: 5px; 121 | -moz-border-radius: 5px; 122 | -webkit-border-radius: 5px; 123 | } 124 | 125 | #qunit-tests table { 126 | border-collapse: collapse; 127 | margin-top: .2em; 128 | } 129 | 130 | #qunit-tests th { 131 | text-align: right; 132 | vertical-align: top; 133 | padding: 0 .5em 0 0; 134 | } 135 | 136 | #qunit-tests td { 137 | vertical-align: top; 138 | } 139 | 140 | #qunit-tests pre { 141 | margin: 0; 142 | white-space: pre-wrap; 143 | word-wrap: break-word; 144 | } 145 | 146 | #qunit-tests del { 147 | background-color: #e0f2be; 148 | color: #374e0c; 149 | text-decoration: none; 150 | } 151 | 152 | #qunit-tests ins { 153 | background-color: #ffcaca; 154 | color: #500; 155 | text-decoration: none; 156 | } 157 | 158 | /*** Test Counts */ 159 | 160 | #qunit-tests b.counts { color: black; } 161 | #qunit-tests b.passed { color: #5E740B; } 162 | #qunit-tests b.failed { color: #710909; } 163 | 164 | #qunit-tests li li { 165 | padding: 5px; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #3c510c; 175 | background-color: #fff; 176 | border-left: 10px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 10px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 5px 5px; 198 | -moz-border-radius: 0 0 5px 5px; 199 | -webkit-border-bottom-right-radius: 5px; 200 | -webkit-border-bottom-left-radius: 5px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | #qunit-testresult .module-name { 224 | font-weight: bold; 225 | } 226 | 227 | /** Fixture */ 228 | 229 | #qunit-fixture { 230 | position: absolute; 231 | top: -10000px; 232 | left: -10000px; 233 | width: 1000px; 234 | height: 1000px; 235 | } 236 | -------------------------------------------------------------------------------- /test/js/qunit-1.10.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | config, 15 | onErrorFnPrev, 16 | testId = 0, 17 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 18 | toString = Object.prototype.toString, 19 | hasOwn = Object.prototype.hasOwnProperty, 20 | // Keep a local reference to Date (GH-283) 21 | Date = window.Date, 22 | defined = { 23 | setTimeout: typeof window.setTimeout !== "undefined", 24 | sessionStorage: (function() { 25 | var x = "qunit-test-string"; 26 | try { 27 | sessionStorage.setItem( x, x ); 28 | sessionStorage.removeItem( x ); 29 | return true; 30 | } catch( e ) { 31 | return false; 32 | } 33 | }()) 34 | }; 35 | 36 | function Test( settings ) { 37 | extend( this, settings ); 38 | this.assertions = []; 39 | this.testNumber = ++Test.count; 40 | } 41 | 42 | Test.count = 0; 43 | 44 | Test.prototype = { 45 | init: function() { 46 | var a, b, li, 47 | tests = id( "qunit-tests" ); 48 | 49 | if ( tests ) { 50 | b = document.createElement( "strong" ); 51 | b.innerHTML = this.name; 52 | 53 | // `a` initialized at top of scope 54 | a = document.createElement( "a" ); 55 | a.innerHTML = "Rerun"; 56 | a.href = QUnit.url({ testNumber: this.testNumber }); 57 | 58 | li = document.createElement( "li" ); 59 | li.appendChild( b ); 60 | li.appendChild( a ); 61 | li.className = "running"; 62 | li.id = this.id = "qunit-test-output" + testId++; 63 | 64 | tests.appendChild( li ); 65 | } 66 | }, 67 | setup: function() { 68 | if ( this.module !== config.previousModule ) { 69 | if ( config.previousModule ) { 70 | runLoggingCallbacks( "moduleDone", QUnit, { 71 | name: config.previousModule, 72 | failed: config.moduleStats.bad, 73 | passed: config.moduleStats.all - config.moduleStats.bad, 74 | total: config.moduleStats.all 75 | }); 76 | } 77 | config.previousModule = this.module; 78 | config.moduleStats = { all: 0, bad: 0 }; 79 | runLoggingCallbacks( "moduleStart", QUnit, { 80 | name: this.module 81 | }); 82 | } else if ( config.autorun ) { 83 | runLoggingCallbacks( "moduleStart", QUnit, { 84 | name: this.module 85 | }); 86 | } 87 | 88 | config.current = this; 89 | 90 | this.testEnvironment = extend({ 91 | setup: function() {}, 92 | teardown: function() {} 93 | }, this.moduleTestEnvironment ); 94 | 95 | runLoggingCallbacks( "testStart", QUnit, { 96 | name: this.testName, 97 | module: this.module 98 | }); 99 | 100 | // allow utility functions to access the current test environment 101 | // TODO why?? 102 | QUnit.current_testEnvironment = this.testEnvironment; 103 | 104 | if ( !config.pollution ) { 105 | saveGlobal(); 106 | } 107 | if ( config.notrycatch ) { 108 | this.testEnvironment.setup.call( this.testEnvironment ); 109 | return; 110 | } 111 | try { 112 | this.testEnvironment.setup.call( this.testEnvironment ); 113 | } catch( e ) { 114 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 115 | } 116 | }, 117 | run: function() { 118 | config.current = this; 119 | 120 | var running = id( "qunit-testresult" ); 121 | 122 | if ( running ) { 123 | running.innerHTML = "Running:
" + this.name; 124 | } 125 | 126 | if ( this.async ) { 127 | QUnit.stop(); 128 | } 129 | 130 | if ( config.notrycatch ) { 131 | this.callback.call( this.testEnvironment, QUnit.assert ); 132 | return; 133 | } 134 | 135 | try { 136 | this.callback.call( this.testEnvironment, QUnit.assert ); 137 | } catch( e ) { 138 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + e.message, extractStacktrace( e, 0 ) ); 139 | // else next test will carry the responsibility 140 | saveGlobal(); 141 | 142 | // Restart the tests if they're blocking 143 | if ( config.blocking ) { 144 | QUnit.start(); 145 | } 146 | } 147 | }, 148 | teardown: function() { 149 | config.current = this; 150 | if ( config.notrycatch ) { 151 | this.testEnvironment.teardown.call( this.testEnvironment ); 152 | return; 153 | } else { 154 | try { 155 | this.testEnvironment.teardown.call( this.testEnvironment ); 156 | } catch( e ) { 157 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + e.message, extractStacktrace( e, 1 ) ); 158 | } 159 | } 160 | checkPollution(); 161 | }, 162 | finish: function() { 163 | config.current = this; 164 | if ( config.requireExpects && this.expected == null ) { 165 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 166 | } else if ( this.expected != null && this.expected != this.assertions.length ) { 167 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 168 | } else if ( this.expected == null && !this.assertions.length ) { 169 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 170 | } 171 | 172 | var assertion, a, b, i, li, ol, 173 | test = this, 174 | good = 0, 175 | bad = 0, 176 | tests = id( "qunit-tests" ); 177 | 178 | config.stats.all += this.assertions.length; 179 | config.moduleStats.all += this.assertions.length; 180 | 181 | if ( tests ) { 182 | ol = document.createElement( "ol" ); 183 | 184 | for ( i = 0; i < this.assertions.length; i++ ) { 185 | assertion = this.assertions[i]; 186 | 187 | li = document.createElement( "li" ); 188 | li.className = assertion.result ? "pass" : "fail"; 189 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 190 | ol.appendChild( li ); 191 | 192 | if ( assertion.result ) { 193 | good++; 194 | } else { 195 | bad++; 196 | config.stats.bad++; 197 | config.moduleStats.bad++; 198 | } 199 | } 200 | 201 | // store result when possible 202 | if ( QUnit.config.reorder && defined.sessionStorage ) { 203 | if ( bad ) { 204 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 205 | } else { 206 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 207 | } 208 | } 209 | 210 | if ( bad === 0 ) { 211 | ol.style.display = "none"; 212 | } 213 | 214 | // `b` initialized at top of scope 215 | b = document.createElement( "strong" ); 216 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 217 | 218 | addEvent(b, "click", function() { 219 | var next = b.nextSibling.nextSibling, 220 | display = next.style.display; 221 | next.style.display = display === "none" ? "block" : "none"; 222 | }); 223 | 224 | addEvent(b, "dblclick", function( e ) { 225 | var target = e && e.target ? e.target : window.event.srcElement; 226 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 227 | target = target.parentNode; 228 | } 229 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 230 | window.location = QUnit.url({ testNumber: test.testNumber }); 231 | } 232 | }); 233 | 234 | // `li` initialized at top of scope 235 | li = id( this.id ); 236 | li.className = bad ? "fail" : "pass"; 237 | li.removeChild( li.firstChild ); 238 | a = li.firstChild; 239 | li.appendChild( b ); 240 | li.appendChild ( a ); 241 | li.appendChild( ol ); 242 | 243 | } else { 244 | for ( i = 0; i < this.assertions.length; i++ ) { 245 | if ( !this.assertions[i].result ) { 246 | bad++; 247 | config.stats.bad++; 248 | config.moduleStats.bad++; 249 | } 250 | } 251 | } 252 | 253 | runLoggingCallbacks( "testDone", QUnit, { 254 | name: this.testName, 255 | module: this.module, 256 | failed: bad, 257 | passed: this.assertions.length - bad, 258 | total: this.assertions.length 259 | }); 260 | 261 | QUnit.reset(); 262 | 263 | config.current = undefined; 264 | }, 265 | 266 | queue: function() { 267 | var bad, 268 | test = this; 269 | 270 | synchronize(function() { 271 | test.init(); 272 | }); 273 | function run() { 274 | // each of these can by async 275 | synchronize(function() { 276 | test.setup(); 277 | }); 278 | synchronize(function() { 279 | test.run(); 280 | }); 281 | synchronize(function() { 282 | test.teardown(); 283 | }); 284 | synchronize(function() { 285 | test.finish(); 286 | }); 287 | } 288 | 289 | // `bad` initialized at top of scope 290 | // defer when previous test run passed, if storage is available 291 | bad = QUnit.config.reorder && defined.sessionStorage && 292 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 293 | 294 | if ( bad ) { 295 | run(); 296 | } else { 297 | synchronize( run, true ); 298 | } 299 | } 300 | }; 301 | 302 | // Root QUnit object. 303 | // `QUnit` initialized at top of scope 304 | QUnit = { 305 | 306 | // call on start of module test to prepend name to all tests 307 | module: function( name, testEnvironment ) { 308 | config.currentModule = name; 309 | config.currentModuleTestEnvironment = testEnvironment; 310 | config.modules[name] = true; 311 | }, 312 | 313 | asyncTest: function( testName, expected, callback ) { 314 | if ( arguments.length === 2 ) { 315 | callback = expected; 316 | expected = null; 317 | } 318 | 319 | QUnit.test( testName, expected, callback, true ); 320 | }, 321 | 322 | test: function( testName, expected, callback, async ) { 323 | var test, 324 | name = "" + escapeInnerText( testName ) + ""; 325 | 326 | if ( arguments.length === 2 ) { 327 | callback = expected; 328 | expected = null; 329 | } 330 | 331 | if ( config.currentModule ) { 332 | name = "" + config.currentModule + ": " + name; 333 | } 334 | 335 | test = new Test({ 336 | name: name, 337 | testName: testName, 338 | expected: expected, 339 | async: async, 340 | callback: callback, 341 | module: config.currentModule, 342 | moduleTestEnvironment: config.currentModuleTestEnvironment, 343 | stack: sourceFromStacktrace( 2 ) 344 | }); 345 | 346 | if ( !validTest( test ) ) { 347 | return; 348 | } 349 | 350 | test.queue(); 351 | }, 352 | 353 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 354 | expect: function( asserts ) { 355 | if (arguments.length === 1) { 356 | config.current.expected = asserts; 357 | } else { 358 | return config.current.expected; 359 | } 360 | }, 361 | 362 | start: function( count ) { 363 | config.semaphore -= count || 1; 364 | // don't start until equal number of stop-calls 365 | if ( config.semaphore > 0 ) { 366 | return; 367 | } 368 | // ignore if start is called more often then stop 369 | if ( config.semaphore < 0 ) { 370 | config.semaphore = 0; 371 | } 372 | // A slight delay, to avoid any current callbacks 373 | if ( defined.setTimeout ) { 374 | window.setTimeout(function() { 375 | if ( config.semaphore > 0 ) { 376 | return; 377 | } 378 | if ( config.timeout ) { 379 | clearTimeout( config.timeout ); 380 | } 381 | 382 | config.blocking = false; 383 | process( true ); 384 | }, 13); 385 | } else { 386 | config.blocking = false; 387 | process( true ); 388 | } 389 | }, 390 | 391 | stop: function( count ) { 392 | config.semaphore += count || 1; 393 | config.blocking = true; 394 | 395 | if ( config.testTimeout && defined.setTimeout ) { 396 | clearTimeout( config.timeout ); 397 | config.timeout = window.setTimeout(function() { 398 | QUnit.ok( false, "Test timed out" ); 399 | config.semaphore = 1; 400 | QUnit.start(); 401 | }, config.testTimeout ); 402 | } 403 | } 404 | }; 405 | 406 | // Asssert helpers 407 | // All of these must call either QUnit.push() or manually do: 408 | // - runLoggingCallbacks( "log", .. ); 409 | // - config.current.assertions.push({ .. }); 410 | QUnit.assert = { 411 | /** 412 | * Asserts rough true-ish result. 413 | * @name ok 414 | * @function 415 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 416 | */ 417 | ok: function( result, msg ) { 418 | if ( !config.current ) { 419 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 420 | } 421 | result = !!result; 422 | 423 | var source, 424 | details = { 425 | module: config.current.module, 426 | name: config.current.testName, 427 | result: result, 428 | message: msg 429 | }; 430 | 431 | msg = escapeInnerText( msg || (result ? "okay" : "failed" ) ); 432 | msg = "" + msg + ""; 433 | 434 | if ( !result ) { 435 | source = sourceFromStacktrace( 2 ); 436 | if ( source ) { 437 | details.source = source; 438 | msg += "
Source:
" + escapeInnerText( source ) + "
"; 439 | } 440 | } 441 | runLoggingCallbacks( "log", QUnit, details ); 442 | config.current.assertions.push({ 443 | result: result, 444 | message: msg 445 | }); 446 | }, 447 | 448 | /** 449 | * Assert that the first two arguments are equal, with an optional message. 450 | * Prints out both actual and expected values. 451 | * @name equal 452 | * @function 453 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 454 | */ 455 | equal: function( actual, expected, message ) { 456 | QUnit.push( expected == actual, actual, expected, message ); 457 | }, 458 | 459 | /** 460 | * @name notEqual 461 | * @function 462 | */ 463 | notEqual: function( actual, expected, message ) { 464 | QUnit.push( expected != actual, actual, expected, message ); 465 | }, 466 | 467 | /** 468 | * @name deepEqual 469 | * @function 470 | */ 471 | deepEqual: function( actual, expected, message ) { 472 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 473 | }, 474 | 475 | /** 476 | * @name notDeepEqual 477 | * @function 478 | */ 479 | notDeepEqual: function( actual, expected, message ) { 480 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 481 | }, 482 | 483 | /** 484 | * @name strictEqual 485 | * @function 486 | */ 487 | strictEqual: function( actual, expected, message ) { 488 | QUnit.push( expected === actual, actual, expected, message ); 489 | }, 490 | 491 | /** 492 | * @name notStrictEqual 493 | * @function 494 | */ 495 | notStrictEqual: function( actual, expected, message ) { 496 | QUnit.push( expected !== actual, actual, expected, message ); 497 | }, 498 | 499 | throws: function( block, expected, message ) { 500 | var actual, 501 | ok = false; 502 | 503 | // 'expected' is optional 504 | if ( typeof expected === "string" ) { 505 | message = expected; 506 | expected = null; 507 | } 508 | 509 | config.current.ignoreGlobalErrors = true; 510 | try { 511 | block.call( config.current.testEnvironment ); 512 | } catch (e) { 513 | actual = e; 514 | } 515 | config.current.ignoreGlobalErrors = false; 516 | 517 | if ( actual ) { 518 | // we don't want to validate thrown error 519 | if ( !expected ) { 520 | ok = true; 521 | // expected is a regexp 522 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 523 | ok = expected.test( actual ); 524 | // expected is a constructor 525 | } else if ( actual instanceof expected ) { 526 | ok = true; 527 | // expected is a validation function which returns true is validation passed 528 | } else if ( expected.call( {}, actual ) === true ) { 529 | ok = true; 530 | } 531 | 532 | QUnit.push( ok, actual, null, message ); 533 | } else { 534 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 535 | } 536 | } 537 | }; 538 | 539 | /** 540 | * @deprecate since 1.8.0 541 | * Kept assertion helpers in root for backwards compatibility 542 | */ 543 | extend( QUnit, QUnit.assert ); 544 | 545 | /** 546 | * @deprecated since 1.9.0 547 | * Kept global "raises()" for backwards compatibility 548 | */ 549 | QUnit.raises = QUnit.assert.throws; 550 | 551 | /** 552 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 553 | * Kept to avoid TypeErrors for undefined methods. 554 | */ 555 | QUnit.equals = function() { 556 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 557 | }; 558 | QUnit.same = function() { 559 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 560 | }; 561 | 562 | // We want access to the constructor's prototype 563 | (function() { 564 | function F() {} 565 | F.prototype = QUnit; 566 | QUnit = new F(); 567 | // Make F QUnit's constructor so that we can add to the prototype later 568 | QUnit.constructor = F; 569 | }()); 570 | 571 | /** 572 | * Config object: Maintain internal state 573 | * Later exposed as QUnit.config 574 | * `config` initialized at top of scope 575 | */ 576 | config = { 577 | // The queue of tests to run 578 | queue: [], 579 | 580 | // block until document ready 581 | blocking: true, 582 | 583 | // when enabled, show only failing tests 584 | // gets persisted through sessionStorage and can be changed in UI via checkbox 585 | hidepassed: false, 586 | 587 | // by default, run previously failed tests first 588 | // very useful in combination with "Hide passed tests" checked 589 | reorder: true, 590 | 591 | // by default, modify document.title when suite is done 592 | altertitle: true, 593 | 594 | // when enabled, all tests must call expect() 595 | requireExpects: false, 596 | 597 | // add checkboxes that are persisted in the query-string 598 | // when enabled, the id is set to `true` as a `QUnit.config` property 599 | urlConfig: [ 600 | { 601 | id: "noglobals", 602 | label: "Check for Globals", 603 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 604 | }, 605 | { 606 | id: "notrycatch", 607 | label: "No try-catch", 608 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 609 | } 610 | ], 611 | 612 | // Set of all modules. 613 | modules: {}, 614 | 615 | // logging callback queues 616 | begin: [], 617 | done: [], 618 | log: [], 619 | testStart: [], 620 | testDone: [], 621 | moduleStart: [], 622 | moduleDone: [] 623 | }; 624 | 625 | // Initialize more QUnit.config and QUnit.urlParams 626 | (function() { 627 | var i, 628 | location = window.location || { search: "", protocol: "file:" }, 629 | params = location.search.slice( 1 ).split( "&" ), 630 | length = params.length, 631 | urlParams = {}, 632 | current; 633 | 634 | if ( params[ 0 ] ) { 635 | for ( i = 0; i < length; i++ ) { 636 | current = params[ i ].split( "=" ); 637 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 638 | // allow just a key to turn on a flag, e.g., test.html?noglobals 639 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 640 | urlParams[ current[ 0 ] ] = current[ 1 ]; 641 | } 642 | } 643 | 644 | QUnit.urlParams = urlParams; 645 | 646 | // String search anywhere in moduleName+testName 647 | config.filter = urlParams.filter; 648 | 649 | // Exact match of the module name 650 | config.module = urlParams.module; 651 | 652 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 653 | 654 | // Figure out if we're running the tests from a server or not 655 | QUnit.isLocal = location.protocol === "file:"; 656 | }()); 657 | 658 | // Export global variables, unless an 'exports' object exists, 659 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 660 | if ( typeof exports === "undefined" ) { 661 | extend( window, QUnit ); 662 | 663 | // Expose QUnit object 664 | window.QUnit = QUnit; 665 | } 666 | 667 | // Extend QUnit object, 668 | // these after set here because they should not be exposed as global functions 669 | extend( QUnit, { 670 | config: config, 671 | 672 | // Initialize the configuration options 673 | init: function() { 674 | extend( config, { 675 | stats: { all: 0, bad: 0 }, 676 | moduleStats: { all: 0, bad: 0 }, 677 | started: +new Date(), 678 | updateRate: 1000, 679 | blocking: false, 680 | autostart: true, 681 | autorun: false, 682 | filter: "", 683 | queue: [], 684 | semaphore: 0 685 | }); 686 | 687 | var tests, banner, result, 688 | qunit = id( "qunit" ); 689 | 690 | if ( qunit ) { 691 | qunit.innerHTML = 692 | "

" + escapeInnerText( document.title ) + "

" + 693 | "

" + 694 | "
" + 695 | "

" + 696 | "
    "; 697 | } 698 | 699 | tests = id( "qunit-tests" ); 700 | banner = id( "qunit-banner" ); 701 | result = id( "qunit-testresult" ); 702 | 703 | if ( tests ) { 704 | tests.innerHTML = ""; 705 | } 706 | 707 | if ( banner ) { 708 | banner.className = ""; 709 | } 710 | 711 | if ( result ) { 712 | result.parentNode.removeChild( result ); 713 | } 714 | 715 | if ( tests ) { 716 | result = document.createElement( "p" ); 717 | result.id = "qunit-testresult"; 718 | result.className = "result"; 719 | tests.parentNode.insertBefore( result, tests ); 720 | result.innerHTML = "Running...
     "; 721 | } 722 | }, 723 | 724 | // Resets the test setup. Useful for tests that modify the DOM. 725 | reset: function() { 726 | var fixture = id( "qunit-fixture" ); 727 | if ( fixture ) { 728 | fixture.innerHTML = config.fixture; 729 | } 730 | }, 731 | 732 | // Trigger an event on an element. 733 | // @example triggerEvent( document.body, "click" ); 734 | triggerEvent: function( elem, type, event ) { 735 | if ( document.createEvent ) { 736 | event = document.createEvent( "MouseEvents" ); 737 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 738 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 739 | 740 | elem.dispatchEvent( event ); 741 | } else if ( elem.fireEvent ) { 742 | elem.fireEvent( "on" + type ); 743 | } 744 | }, 745 | 746 | // Safe object type checking 747 | is: function( type, obj ) { 748 | return QUnit.objectType( obj ) == type; 749 | }, 750 | 751 | objectType: function( obj ) { 752 | if ( typeof obj === "undefined" ) { 753 | return "undefined"; 754 | // consider: typeof null === object 755 | } 756 | if ( obj === null ) { 757 | return "null"; 758 | } 759 | 760 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ""; 761 | 762 | switch ( type ) { 763 | case "Number": 764 | if ( isNaN(obj) ) { 765 | return "nan"; 766 | } 767 | return "number"; 768 | case "String": 769 | case "Boolean": 770 | case "Array": 771 | case "Date": 772 | case "RegExp": 773 | case "Function": 774 | return type.toLowerCase(); 775 | } 776 | if ( typeof obj === "object" ) { 777 | return "object"; 778 | } 779 | return undefined; 780 | }, 781 | 782 | push: function( result, actual, expected, message ) { 783 | if ( !config.current ) { 784 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 785 | } 786 | 787 | var output, source, 788 | details = { 789 | module: config.current.module, 790 | name: config.current.testName, 791 | result: result, 792 | message: message, 793 | actual: actual, 794 | expected: expected 795 | }; 796 | 797 | message = escapeInnerText( message ) || ( result ? "okay" : "failed" ); 798 | message = "" + message + ""; 799 | output = message; 800 | 801 | if ( !result ) { 802 | expected = escapeInnerText( QUnit.jsDump.parse(expected) ); 803 | actual = escapeInnerText( QUnit.jsDump.parse(actual) ); 804 | output += ""; 805 | 806 | if ( actual != expected ) { 807 | output += ""; 808 | output += ""; 809 | } 810 | 811 | source = sourceFromStacktrace(); 812 | 813 | if ( source ) { 814 | details.source = source; 815 | output += ""; 816 | } 817 | 818 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeInnerText( source ) + "
    "; 819 | } 820 | 821 | runLoggingCallbacks( "log", QUnit, details ); 822 | 823 | config.current.assertions.push({ 824 | result: !!result, 825 | message: output 826 | }); 827 | }, 828 | 829 | pushFailure: function( message, source, actual ) { 830 | if ( !config.current ) { 831 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 832 | } 833 | 834 | var output, 835 | details = { 836 | module: config.current.module, 837 | name: config.current.testName, 838 | result: false, 839 | message: message 840 | }; 841 | 842 | message = escapeInnerText( message ) || "error"; 843 | message = "" + message + ""; 844 | output = message; 845 | 846 | output += ""; 847 | 848 | if ( actual ) { 849 | output += ""; 850 | } 851 | 852 | if ( source ) { 853 | details.source = source; 854 | output += ""; 855 | } 856 | 857 | output += "
    Result:
    " + escapeInnerText( actual ) + "
    Source:
    " + escapeInnerText( source ) + "
    "; 858 | 859 | runLoggingCallbacks( "log", QUnit, details ); 860 | 861 | config.current.assertions.push({ 862 | result: false, 863 | message: output 864 | }); 865 | }, 866 | 867 | url: function( params ) { 868 | params = extend( extend( {}, QUnit.urlParams ), params ); 869 | var key, 870 | querystring = "?"; 871 | 872 | for ( key in params ) { 873 | if ( !hasOwn.call( params, key ) ) { 874 | continue; 875 | } 876 | querystring += encodeURIComponent( key ) + "=" + 877 | encodeURIComponent( params[ key ] ) + "&"; 878 | } 879 | return window.location.pathname + querystring.slice( 0, -1 ); 880 | }, 881 | 882 | extend: extend, 883 | id: id, 884 | addEvent: addEvent 885 | // load, equiv, jsDump, diff: Attached later 886 | }); 887 | 888 | /** 889 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 890 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 891 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 892 | * Doing this allows us to tell if the following methods have been overwritten on the actual 893 | * QUnit object. 894 | */ 895 | extend( QUnit.constructor.prototype, { 896 | 897 | // Logging callbacks; all receive a single argument with the listed properties 898 | // run test/logs.html for any related changes 899 | begin: registerLoggingCallback( "begin" ), 900 | 901 | // done: { failed, passed, total, runtime } 902 | done: registerLoggingCallback( "done" ), 903 | 904 | // log: { result, actual, expected, message } 905 | log: registerLoggingCallback( "log" ), 906 | 907 | // testStart: { name } 908 | testStart: registerLoggingCallback( "testStart" ), 909 | 910 | // testDone: { name, failed, passed, total } 911 | testDone: registerLoggingCallback( "testDone" ), 912 | 913 | // moduleStart: { name } 914 | moduleStart: registerLoggingCallback( "moduleStart" ), 915 | 916 | // moduleDone: { name, failed, passed, total } 917 | moduleDone: registerLoggingCallback( "moduleDone" ) 918 | }); 919 | 920 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 921 | config.autorun = true; 922 | } 923 | 924 | QUnit.load = function() { 925 | runLoggingCallbacks( "begin", QUnit, {} ); 926 | 927 | // Initialize the config, saving the execution queue 928 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, urlConfigCheckboxes, moduleFilter, 929 | numModules = 0, 930 | moduleFilterHtml = "", 931 | urlConfigHtml = "", 932 | oldconfig = extend( {}, config ); 933 | 934 | QUnit.init(); 935 | extend(config, oldconfig); 936 | 937 | config.blocking = false; 938 | 939 | len = config.urlConfig.length; 940 | 941 | for ( i = 0; i < len; i++ ) { 942 | val = config.urlConfig[i]; 943 | if ( typeof val === "string" ) { 944 | val = { 945 | id: val, 946 | label: val, 947 | tooltip: "[no tooltip available]" 948 | }; 949 | } 950 | config[ val.id ] = QUnit.urlParams[ val.id ]; 951 | urlConfigHtml += ""; 952 | } 953 | 954 | moduleFilterHtml += ""; 962 | 963 | // `userAgent` initialized at top of scope 964 | userAgent = id( "qunit-userAgent" ); 965 | if ( userAgent ) { 966 | userAgent.innerHTML = navigator.userAgent; 967 | } 968 | 969 | // `banner` initialized at top of scope 970 | banner = id( "qunit-header" ); 971 | if ( banner ) { 972 | banner.innerHTML = "" + banner.innerHTML + " "; 973 | } 974 | 975 | // `toolbar` initialized at top of scope 976 | toolbar = id( "qunit-testrunner-toolbar" ); 977 | if ( toolbar ) { 978 | // `filter` initialized at top of scope 979 | filter = document.createElement( "input" ); 980 | filter.type = "checkbox"; 981 | filter.id = "qunit-filter-pass"; 982 | 983 | addEvent( filter, "click", function() { 984 | var tmp, 985 | ol = document.getElementById( "qunit-tests" ); 986 | 987 | if ( filter.checked ) { 988 | ol.className = ol.className + " hidepass"; 989 | } else { 990 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 991 | ol.className = tmp.replace( / hidepass /, " " ); 992 | } 993 | if ( defined.sessionStorage ) { 994 | if (filter.checked) { 995 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 996 | } else { 997 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 998 | } 999 | } 1000 | }); 1001 | 1002 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1003 | filter.checked = true; 1004 | // `ol` initialized at top of scope 1005 | ol = document.getElementById( "qunit-tests" ); 1006 | ol.className = ol.className + " hidepass"; 1007 | } 1008 | toolbar.appendChild( filter ); 1009 | 1010 | // `label` initialized at top of scope 1011 | label = document.createElement( "label" ); 1012 | label.setAttribute( "for", "qunit-filter-pass" ); 1013 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); 1014 | label.innerHTML = "Hide passed tests"; 1015 | toolbar.appendChild( label ); 1016 | 1017 | urlConfigCheckboxes = document.createElement( 'span' ); 1018 | urlConfigCheckboxes.innerHTML = urlConfigHtml; 1019 | addEvent( urlConfigCheckboxes, "change", function( event ) { 1020 | var params = {}; 1021 | params[ event.target.name ] = event.target.checked ? true : undefined; 1022 | window.location = QUnit.url( params ); 1023 | }); 1024 | toolbar.appendChild( urlConfigCheckboxes ); 1025 | 1026 | if (numModules > 1) { 1027 | moduleFilter = document.createElement( 'span' ); 1028 | moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); 1029 | moduleFilter.innerHTML = moduleFilterHtml; 1030 | addEvent( moduleFilter, "change", function() { 1031 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1032 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1033 | 1034 | window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); 1035 | }); 1036 | toolbar.appendChild(moduleFilter); 1037 | } 1038 | } 1039 | 1040 | // `main` initialized at top of scope 1041 | main = id( "qunit-fixture" ); 1042 | if ( main ) { 1043 | config.fixture = main.innerHTML; 1044 | } 1045 | 1046 | if ( config.autostart ) { 1047 | QUnit.start(); 1048 | } 1049 | }; 1050 | 1051 | addEvent( window, "load", QUnit.load ); 1052 | 1053 | // `onErrorFnPrev` initialized at top of scope 1054 | // Preserve other handlers 1055 | onErrorFnPrev = window.onerror; 1056 | 1057 | // Cover uncaught exceptions 1058 | // Returning true will surpress the default browser handler, 1059 | // returning false will let it run. 1060 | window.onerror = function ( error, filePath, linerNr ) { 1061 | var ret = false; 1062 | if ( onErrorFnPrev ) { 1063 | ret = onErrorFnPrev( error, filePath, linerNr ); 1064 | } 1065 | 1066 | // Treat return value as window.onerror itself does, 1067 | // Only do our handling if not surpressed. 1068 | if ( ret !== true ) { 1069 | if ( QUnit.config.current ) { 1070 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1071 | return true; 1072 | } 1073 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1074 | } else { 1075 | QUnit.test( "global failure", extend( function() { 1076 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1077 | }, { validTest: validTest } ) ); 1078 | } 1079 | return false; 1080 | } 1081 | 1082 | return ret; 1083 | }; 1084 | 1085 | function done() { 1086 | config.autorun = true; 1087 | 1088 | // Log the last module results 1089 | if ( config.currentModule ) { 1090 | runLoggingCallbacks( "moduleDone", QUnit, { 1091 | name: config.currentModule, 1092 | failed: config.moduleStats.bad, 1093 | passed: config.moduleStats.all - config.moduleStats.bad, 1094 | total: config.moduleStats.all 1095 | }); 1096 | } 1097 | 1098 | var i, key, 1099 | banner = id( "qunit-banner" ), 1100 | tests = id( "qunit-tests" ), 1101 | runtime = +new Date() - config.started, 1102 | passed = config.stats.all - config.stats.bad, 1103 | html = [ 1104 | "Tests completed in ", 1105 | runtime, 1106 | " milliseconds.
    ", 1107 | "", 1108 | passed, 1109 | " tests of ", 1110 | config.stats.all, 1111 | " passed, ", 1112 | config.stats.bad, 1113 | " failed." 1114 | ].join( "" ); 1115 | 1116 | if ( banner ) { 1117 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1118 | } 1119 | 1120 | if ( tests ) { 1121 | id( "qunit-testresult" ).innerHTML = html; 1122 | } 1123 | 1124 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1125 | // show ✖ for good, ✔ for bad suite result in title 1126 | // use escape sequences in case file gets loaded with non-utf-8-charset 1127 | document.title = [ 1128 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1129 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1130 | ].join( " " ); 1131 | } 1132 | 1133 | // clear own sessionStorage items if all tests passed 1134 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1135 | // `key` & `i` initialized at top of scope 1136 | for ( i = 0; i < sessionStorage.length; i++ ) { 1137 | key = sessionStorage.key( i++ ); 1138 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1139 | sessionStorage.removeItem( key ); 1140 | } 1141 | } 1142 | } 1143 | 1144 | // scroll back to top to show results 1145 | if ( window.scrollTo ) { 1146 | window.scrollTo(0, 0); 1147 | } 1148 | 1149 | runLoggingCallbacks( "done", QUnit, { 1150 | failed: config.stats.bad, 1151 | passed: passed, 1152 | total: config.stats.all, 1153 | runtime: runtime 1154 | }); 1155 | } 1156 | 1157 | /** @return Boolean: true if this test should be ran */ 1158 | function validTest( test ) { 1159 | var include, 1160 | filter = config.filter && config.filter.toLowerCase(), 1161 | module = config.module && config.module.toLowerCase(), 1162 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1163 | 1164 | // Internally-generated tests are always valid 1165 | if ( test.callback && test.callback.validTest === validTest ) { 1166 | delete test.callback.validTest; 1167 | return true; 1168 | } 1169 | 1170 | if ( config.testNumber ) { 1171 | return test.testNumber === config.testNumber; 1172 | } 1173 | 1174 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1175 | return false; 1176 | } 1177 | 1178 | if ( !filter ) { 1179 | return true; 1180 | } 1181 | 1182 | include = filter.charAt( 0 ) !== "!"; 1183 | if ( !include ) { 1184 | filter = filter.slice( 1 ); 1185 | } 1186 | 1187 | // If the filter matches, we need to honour include 1188 | if ( fullName.indexOf( filter ) !== -1 ) { 1189 | return include; 1190 | } 1191 | 1192 | // Otherwise, do the opposite 1193 | return !include; 1194 | } 1195 | 1196 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1197 | // Later Safari and IE10 are supposed to support error.stack as well 1198 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1199 | function extractStacktrace( e, offset ) { 1200 | offset = offset === undefined ? 3 : offset; 1201 | 1202 | var stack, include, i, regex; 1203 | 1204 | if ( e.stacktrace ) { 1205 | // Opera 1206 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1207 | } else if ( e.stack ) { 1208 | // Firefox, Chrome 1209 | stack = e.stack.split( "\n" ); 1210 | if (/^error$/i.test( stack[0] ) ) { 1211 | stack.shift(); 1212 | } 1213 | if ( fileName ) { 1214 | include = []; 1215 | for ( i = offset; i < stack.length; i++ ) { 1216 | if ( stack[ i ].indexOf( fileName ) != -1 ) { 1217 | break; 1218 | } 1219 | include.push( stack[ i ] ); 1220 | } 1221 | if ( include.length ) { 1222 | return include.join( "\n" ); 1223 | } 1224 | } 1225 | return stack[ offset ]; 1226 | } else if ( e.sourceURL ) { 1227 | // Safari, PhantomJS 1228 | // hopefully one day Safari provides actual stacktraces 1229 | // exclude useless self-reference for generated Error objects 1230 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1231 | return; 1232 | } 1233 | // for actual exceptions, this is useful 1234 | return e.sourceURL + ":" + e.line; 1235 | } 1236 | } 1237 | function sourceFromStacktrace( offset ) { 1238 | try { 1239 | throw new Error(); 1240 | } catch ( e ) { 1241 | return extractStacktrace( e, offset ); 1242 | } 1243 | } 1244 | 1245 | function escapeInnerText( s ) { 1246 | if ( !s ) { 1247 | return ""; 1248 | } 1249 | s = s + ""; 1250 | return s.replace( /[\&<>]/g, function( s ) { 1251 | switch( s ) { 1252 | case "&": return "&"; 1253 | case "<": return "<"; 1254 | case ">": return ">"; 1255 | default: return s; 1256 | } 1257 | }); 1258 | } 1259 | 1260 | function synchronize( callback, last ) { 1261 | config.queue.push( callback ); 1262 | 1263 | if ( config.autorun && !config.blocking ) { 1264 | process( last ); 1265 | } 1266 | } 1267 | 1268 | function process( last ) { 1269 | function next() { 1270 | process( last ); 1271 | } 1272 | var start = new Date().getTime(); 1273 | config.depth = config.depth ? config.depth + 1 : 1; 1274 | 1275 | while ( config.queue.length && !config.blocking ) { 1276 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1277 | config.queue.shift()(); 1278 | } else { 1279 | window.setTimeout( next, 13 ); 1280 | break; 1281 | } 1282 | } 1283 | config.depth--; 1284 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1285 | done(); 1286 | } 1287 | } 1288 | 1289 | function saveGlobal() { 1290 | config.pollution = []; 1291 | 1292 | if ( config.noglobals ) { 1293 | for ( var key in window ) { 1294 | // in Opera sometimes DOM element ids show up here, ignore them 1295 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { 1296 | continue; 1297 | } 1298 | config.pollution.push( key ); 1299 | } 1300 | } 1301 | } 1302 | 1303 | function checkPollution( name ) { 1304 | var newGlobals, 1305 | deletedGlobals, 1306 | old = config.pollution; 1307 | 1308 | saveGlobal(); 1309 | 1310 | newGlobals = diff( config.pollution, old ); 1311 | if ( newGlobals.length > 0 ) { 1312 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1313 | } 1314 | 1315 | deletedGlobals = diff( old, config.pollution ); 1316 | if ( deletedGlobals.length > 0 ) { 1317 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1318 | } 1319 | } 1320 | 1321 | // returns a new Array with the elements that are in a but not in b 1322 | function diff( a, b ) { 1323 | var i, j, 1324 | result = a.slice(); 1325 | 1326 | for ( i = 0; i < result.length; i++ ) { 1327 | for ( j = 0; j < b.length; j++ ) { 1328 | if ( result[i] === b[j] ) { 1329 | result.splice( i, 1 ); 1330 | i--; 1331 | break; 1332 | } 1333 | } 1334 | } 1335 | return result; 1336 | } 1337 | 1338 | function extend( a, b ) { 1339 | for ( var prop in b ) { 1340 | if ( b[ prop ] === undefined ) { 1341 | delete a[ prop ]; 1342 | 1343 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1344 | } else if ( prop !== "constructor" || a !== window ) { 1345 | a[ prop ] = b[ prop ]; 1346 | } 1347 | } 1348 | 1349 | return a; 1350 | } 1351 | 1352 | function addEvent( elem, type, fn ) { 1353 | if ( elem.addEventListener ) { 1354 | elem.addEventListener( type, fn, false ); 1355 | } else if ( elem.attachEvent ) { 1356 | elem.attachEvent( "on" + type, fn ); 1357 | } else { 1358 | fn(); 1359 | } 1360 | } 1361 | 1362 | function id( name ) { 1363 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1364 | document.getElementById( name ); 1365 | } 1366 | 1367 | function registerLoggingCallback( key ) { 1368 | return function( callback ) { 1369 | config[key].push( callback ); 1370 | }; 1371 | } 1372 | 1373 | // Supports deprecated method of completely overwriting logging callbacks 1374 | function runLoggingCallbacks( key, scope, args ) { 1375 | //debugger; 1376 | var i, callbacks; 1377 | if ( QUnit.hasOwnProperty( key ) ) { 1378 | QUnit[ key ].call(scope, args ); 1379 | } else { 1380 | callbacks = config[ key ]; 1381 | for ( i = 0; i < callbacks.length; i++ ) { 1382 | callbacks[ i ].call( scope, args ); 1383 | } 1384 | } 1385 | } 1386 | 1387 | // Test for equality any JavaScript type. 1388 | // Author: Philippe Rathé 1389 | QUnit.equiv = (function() { 1390 | 1391 | // Call the o related callback with the given arguments. 1392 | function bindCallbacks( o, callbacks, args ) { 1393 | var prop = QUnit.objectType( o ); 1394 | if ( prop ) { 1395 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1396 | return callbacks[ prop ].apply( callbacks, args ); 1397 | } else { 1398 | return callbacks[ prop ]; // or undefined 1399 | } 1400 | } 1401 | } 1402 | 1403 | // the real equiv function 1404 | var innerEquiv, 1405 | // stack to decide between skip/abort functions 1406 | callers = [], 1407 | // stack to avoiding loops from circular referencing 1408 | parents = [], 1409 | 1410 | getProto = Object.getPrototypeOf || function ( obj ) { 1411 | return obj.__proto__; 1412 | }, 1413 | callbacks = (function () { 1414 | 1415 | // for string, boolean, number and null 1416 | function useStrictEquality( b, a ) { 1417 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1418 | // to catch short annotaion VS 'new' annotation of a 1419 | // declaration 1420 | // e.g. var i = 1; 1421 | // var j = new Number(1); 1422 | return a == b; 1423 | } else { 1424 | return a === b; 1425 | } 1426 | } 1427 | 1428 | return { 1429 | "string": useStrictEquality, 1430 | "boolean": useStrictEquality, 1431 | "number": useStrictEquality, 1432 | "null": useStrictEquality, 1433 | "undefined": useStrictEquality, 1434 | 1435 | "nan": function( b ) { 1436 | return isNaN( b ); 1437 | }, 1438 | 1439 | "date": function( b, a ) { 1440 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1441 | }, 1442 | 1443 | "regexp": function( b, a ) { 1444 | return QUnit.objectType( b ) === "regexp" && 1445 | // the regex itself 1446 | a.source === b.source && 1447 | // and its modifers 1448 | a.global === b.global && 1449 | // (gmi) ... 1450 | a.ignoreCase === b.ignoreCase && 1451 | a.multiline === b.multiline && 1452 | a.sticky === b.sticky; 1453 | }, 1454 | 1455 | // - skip when the property is a method of an instance (OOP) 1456 | // - abort otherwise, 1457 | // initial === would have catch identical references anyway 1458 | "function": function() { 1459 | var caller = callers[callers.length - 1]; 1460 | return caller !== Object && typeof caller !== "undefined"; 1461 | }, 1462 | 1463 | "array": function( b, a ) { 1464 | var i, j, len, loop; 1465 | 1466 | // b could be an object literal here 1467 | if ( QUnit.objectType( b ) !== "array" ) { 1468 | return false; 1469 | } 1470 | 1471 | len = a.length; 1472 | if ( len !== b.length ) { 1473 | // safe and faster 1474 | return false; 1475 | } 1476 | 1477 | // track reference to avoid circular references 1478 | parents.push( a ); 1479 | for ( i = 0; i < len; i++ ) { 1480 | loop = false; 1481 | for ( j = 0; j < parents.length; j++ ) { 1482 | if ( parents[j] === a[i] ) { 1483 | loop = true;// dont rewalk array 1484 | } 1485 | } 1486 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1487 | parents.pop(); 1488 | return false; 1489 | } 1490 | } 1491 | parents.pop(); 1492 | return true; 1493 | }, 1494 | 1495 | "object": function( b, a ) { 1496 | var i, j, loop, 1497 | // Default to true 1498 | eq = true, 1499 | aProperties = [], 1500 | bProperties = []; 1501 | 1502 | // comparing constructors is more strict than using 1503 | // instanceof 1504 | if ( a.constructor !== b.constructor ) { 1505 | // Allow objects with no prototype to be equivalent to 1506 | // objects with Object as their constructor. 1507 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1508 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1509 | return false; 1510 | } 1511 | } 1512 | 1513 | // stack constructor before traversing properties 1514 | callers.push( a.constructor ); 1515 | // track reference to avoid circular references 1516 | parents.push( a ); 1517 | 1518 | for ( i in a ) { // be strict: don't ensures hasOwnProperty 1519 | // and go deep 1520 | loop = false; 1521 | for ( j = 0; j < parents.length; j++ ) { 1522 | if ( parents[j] === a[i] ) { 1523 | // don't go down the same path twice 1524 | loop = true; 1525 | } 1526 | } 1527 | aProperties.push(i); // collect a's properties 1528 | 1529 | if (!loop && !innerEquiv( a[i], b[i] ) ) { 1530 | eq = false; 1531 | break; 1532 | } 1533 | } 1534 | 1535 | callers.pop(); // unstack, we are done 1536 | parents.pop(); 1537 | 1538 | for ( i in b ) { 1539 | bProperties.push( i ); // collect b's properties 1540 | } 1541 | 1542 | // Ensures identical properties name 1543 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1544 | } 1545 | }; 1546 | }()); 1547 | 1548 | innerEquiv = function() { // can take multiple arguments 1549 | var args = [].slice.apply( arguments ); 1550 | if ( args.length < 2 ) { 1551 | return true; // end transition 1552 | } 1553 | 1554 | return (function( a, b ) { 1555 | if ( a === b ) { 1556 | return true; // catch the most you can 1557 | } else if ( a === null || b === null || typeof a === "undefined" || 1558 | typeof b === "undefined" || 1559 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1560 | return false; // don't lose time with error prone cases 1561 | } else { 1562 | return bindCallbacks(a, callbacks, [ b, a ]); 1563 | } 1564 | 1565 | // apply transition with (1..n) arguments 1566 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); 1567 | }; 1568 | 1569 | return innerEquiv; 1570 | }()); 1571 | 1572 | /** 1573 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1574 | * http://flesler.blogspot.com Licensed under BSD 1575 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1576 | * 1577 | * @projectDescription Advanced and extensible data dumping for Javascript. 1578 | * @version 1.0.0 1579 | * @author Ariel Flesler 1580 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1581 | */ 1582 | QUnit.jsDump = (function() { 1583 | function quote( str ) { 1584 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; 1585 | } 1586 | function literal( o ) { 1587 | return o + ""; 1588 | } 1589 | function join( pre, arr, post ) { 1590 | var s = jsDump.separator(), 1591 | base = jsDump.indent(), 1592 | inner = jsDump.indent(1); 1593 | if ( arr.join ) { 1594 | arr = arr.join( "," + s + inner ); 1595 | } 1596 | if ( !arr ) { 1597 | return pre + post; 1598 | } 1599 | return [ pre, inner + arr, base + post ].join(s); 1600 | } 1601 | function array( arr, stack ) { 1602 | var i = arr.length, ret = new Array(i); 1603 | this.up(); 1604 | while ( i-- ) { 1605 | ret[i] = this.parse( arr[i] , undefined , stack); 1606 | } 1607 | this.down(); 1608 | return join( "[", ret, "]" ); 1609 | } 1610 | 1611 | var reName = /^function (\w+)/, 1612 | jsDump = { 1613 | parse: function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1614 | stack = stack || [ ]; 1615 | var inStack, res, 1616 | parser = this.parsers[ type || this.typeOf(obj) ]; 1617 | 1618 | type = typeof parser; 1619 | inStack = inArray( obj, stack ); 1620 | 1621 | if ( inStack != -1 ) { 1622 | return "recursion(" + (inStack - stack.length) + ")"; 1623 | } 1624 | //else 1625 | if ( type == "function" ) { 1626 | stack.push( obj ); 1627 | res = parser.call( this, obj, stack ); 1628 | stack.pop(); 1629 | return res; 1630 | } 1631 | // else 1632 | return ( type == "string" ) ? parser : this.parsers.error; 1633 | }, 1634 | typeOf: function( obj ) { 1635 | var type; 1636 | if ( obj === null ) { 1637 | type = "null"; 1638 | } else if ( typeof obj === "undefined" ) { 1639 | type = "undefined"; 1640 | } else if ( QUnit.is( "regexp", obj) ) { 1641 | type = "regexp"; 1642 | } else if ( QUnit.is( "date", obj) ) { 1643 | type = "date"; 1644 | } else if ( QUnit.is( "function", obj) ) { 1645 | type = "function"; 1646 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1647 | type = "window"; 1648 | } else if ( obj.nodeType === 9 ) { 1649 | type = "document"; 1650 | } else if ( obj.nodeType ) { 1651 | type = "node"; 1652 | } else if ( 1653 | // native arrays 1654 | toString.call( obj ) === "[object Array]" || 1655 | // NodeList objects 1656 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1657 | ) { 1658 | type = "array"; 1659 | } else { 1660 | type = typeof obj; 1661 | } 1662 | return type; 1663 | }, 1664 | separator: function() { 1665 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1666 | }, 1667 | indent: function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1668 | if ( !this.multiline ) { 1669 | return ""; 1670 | } 1671 | var chr = this.indentChar; 1672 | if ( this.HTML ) { 1673 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1674 | } 1675 | return new Array( this._depth_ + (extra||0) ).join(chr); 1676 | }, 1677 | up: function( a ) { 1678 | this._depth_ += a || 1; 1679 | }, 1680 | down: function( a ) { 1681 | this._depth_ -= a || 1; 1682 | }, 1683 | setParser: function( name, parser ) { 1684 | this.parsers[name] = parser; 1685 | }, 1686 | // The next 3 are exposed so you can use them 1687 | quote: quote, 1688 | literal: literal, 1689 | join: join, 1690 | // 1691 | _depth_: 1, 1692 | // This is the list of parsers, to modify them, use jsDump.setParser 1693 | parsers: { 1694 | window: "[Window]", 1695 | document: "[Document]", 1696 | error: "[ERROR]", //when no parser is found, shouldn"t happen 1697 | unknown: "[Unknown]", 1698 | "null": "null", 1699 | "undefined": "undefined", 1700 | "function": function( fn ) { 1701 | var ret = "function", 1702 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];//functions never have name in IE 1703 | 1704 | if ( name ) { 1705 | ret += " " + name; 1706 | } 1707 | ret += "( "; 1708 | 1709 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1710 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1711 | }, 1712 | array: array, 1713 | nodelist: array, 1714 | "arguments": array, 1715 | object: function( map, stack ) { 1716 | var ret = [ ], keys, key, val, i; 1717 | QUnit.jsDump.up(); 1718 | if ( Object.keys ) { 1719 | keys = Object.keys( map ); 1720 | } else { 1721 | keys = []; 1722 | for ( key in map ) { 1723 | keys.push( key ); 1724 | } 1725 | } 1726 | keys.sort(); 1727 | for ( i = 0; i < keys.length; i++ ) { 1728 | key = keys[ i ]; 1729 | val = map[ key ]; 1730 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1731 | } 1732 | QUnit.jsDump.down(); 1733 | return join( "{", ret, "}" ); 1734 | }, 1735 | node: function( node ) { 1736 | var a, val, 1737 | open = QUnit.jsDump.HTML ? "<" : "<", 1738 | close = QUnit.jsDump.HTML ? ">" : ">", 1739 | tag = node.nodeName.toLowerCase(), 1740 | ret = open + tag; 1741 | 1742 | for ( a in QUnit.jsDump.DOMAttrs ) { 1743 | val = node[ QUnit.jsDump.DOMAttrs[a] ]; 1744 | if ( val ) { 1745 | ret += " " + a + "=" + QUnit.jsDump.parse( val, "attribute" ); 1746 | } 1747 | } 1748 | return ret + close + open + "/" + tag + close; 1749 | }, 1750 | functionArgs: function( fn ) {//function calls it internally, it's the arguments part of the function 1751 | var args, 1752 | l = fn.length; 1753 | 1754 | if ( !l ) { 1755 | return ""; 1756 | } 1757 | 1758 | args = new Array(l); 1759 | while ( l-- ) { 1760 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1761 | } 1762 | return " " + args.join( ", " ) + " "; 1763 | }, 1764 | key: quote, //object calls it internally, the key part of an item in a map 1765 | functionCode: "[code]", //function calls it internally, it's the content of the function 1766 | attribute: quote, //node calls it internally, it's an html attribute value 1767 | string: quote, 1768 | date: quote, 1769 | regexp: literal, //regex 1770 | number: literal, 1771 | "boolean": literal 1772 | }, 1773 | DOMAttrs: { 1774 | //attributes to dump from nodes, name=>realName 1775 | id: "id", 1776 | name: "name", 1777 | "class": "className" 1778 | }, 1779 | HTML: false,//if true, entities are escaped ( <, >, \t, space and \n ) 1780 | indentChar: " ",//indentation unit 1781 | multiline: true //if true, items in a collection, are separated by a \n, else just a space. 1782 | }; 1783 | 1784 | return jsDump; 1785 | }()); 1786 | 1787 | // from Sizzle.js 1788 | function getText( elems ) { 1789 | var i, elem, 1790 | ret = ""; 1791 | 1792 | for ( i = 0; elems[i]; i++ ) { 1793 | elem = elems[i]; 1794 | 1795 | // Get the text from text nodes and CDATA nodes 1796 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1797 | ret += elem.nodeValue; 1798 | 1799 | // Traverse everything else, except comment nodes 1800 | } else if ( elem.nodeType !== 8 ) { 1801 | ret += getText( elem.childNodes ); 1802 | } 1803 | } 1804 | 1805 | return ret; 1806 | } 1807 | 1808 | // from jquery.js 1809 | function inArray( elem, array ) { 1810 | if ( array.indexOf ) { 1811 | return array.indexOf( elem ); 1812 | } 1813 | 1814 | for ( var i = 0, length = array.length; i < length; i++ ) { 1815 | if ( array[ i ] === elem ) { 1816 | return i; 1817 | } 1818 | } 1819 | 1820 | return -1; 1821 | } 1822 | 1823 | /* 1824 | * Javascript Diff Algorithm 1825 | * By John Resig (http://ejohn.org/) 1826 | * Modified by Chu Alan "sprite" 1827 | * 1828 | * Released under the MIT license. 1829 | * 1830 | * More Info: 1831 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1832 | * 1833 | * Usage: QUnit.diff(expected, actual) 1834 | * 1835 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 1836 | */ 1837 | QUnit.diff = (function() { 1838 | function diff( o, n ) { 1839 | var i, 1840 | ns = {}, 1841 | os = {}; 1842 | 1843 | for ( i = 0; i < n.length; i++ ) { 1844 | if ( ns[ n[i] ] == null ) { 1845 | ns[ n[i] ] = { 1846 | rows: [], 1847 | o: null 1848 | }; 1849 | } 1850 | ns[ n[i] ].rows.push( i ); 1851 | } 1852 | 1853 | for ( i = 0; i < o.length; i++ ) { 1854 | if ( os[ o[i] ] == null ) { 1855 | os[ o[i] ] = { 1856 | rows: [], 1857 | n: null 1858 | }; 1859 | } 1860 | os[ o[i] ].rows.push( i ); 1861 | } 1862 | 1863 | for ( i in ns ) { 1864 | if ( !hasOwn.call( ns, i ) ) { 1865 | continue; 1866 | } 1867 | if ( ns[i].rows.length == 1 && typeof os[i] != "undefined" && os[i].rows.length == 1 ) { 1868 | n[ ns[i].rows[0] ] = { 1869 | text: n[ ns[i].rows[0] ], 1870 | row: os[i].rows[0] 1871 | }; 1872 | o[ os[i].rows[0] ] = { 1873 | text: o[ os[i].rows[0] ], 1874 | row: ns[i].rows[0] 1875 | }; 1876 | } 1877 | } 1878 | 1879 | for ( i = 0; i < n.length - 1; i++ ) { 1880 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 1881 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 1882 | 1883 | n[ i + 1 ] = { 1884 | text: n[ i + 1 ], 1885 | row: n[i].row + 1 1886 | }; 1887 | o[ n[i].row + 1 ] = { 1888 | text: o[ n[i].row + 1 ], 1889 | row: i + 1 1890 | }; 1891 | } 1892 | } 1893 | 1894 | for ( i = n.length - 1; i > 0; i-- ) { 1895 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 1896 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 1897 | 1898 | n[ i - 1 ] = { 1899 | text: n[ i - 1 ], 1900 | row: n[i].row - 1 1901 | }; 1902 | o[ n[i].row - 1 ] = { 1903 | text: o[ n[i].row - 1 ], 1904 | row: i - 1 1905 | }; 1906 | } 1907 | } 1908 | 1909 | return { 1910 | o: o, 1911 | n: n 1912 | }; 1913 | } 1914 | 1915 | return function( o, n ) { 1916 | o = o.replace( /\s+$/, "" ); 1917 | n = n.replace( /\s+$/, "" ); 1918 | 1919 | var i, pre, 1920 | str = "", 1921 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 1922 | oSpace = o.match(/\s+/g), 1923 | nSpace = n.match(/\s+/g); 1924 | 1925 | if ( oSpace == null ) { 1926 | oSpace = [ " " ]; 1927 | } 1928 | else { 1929 | oSpace.push( " " ); 1930 | } 1931 | 1932 | if ( nSpace == null ) { 1933 | nSpace = [ " " ]; 1934 | } 1935 | else { 1936 | nSpace.push( " " ); 1937 | } 1938 | 1939 | if ( out.n.length === 0 ) { 1940 | for ( i = 0; i < out.o.length; i++ ) { 1941 | str += "" + out.o[i] + oSpace[i] + ""; 1942 | } 1943 | } 1944 | else { 1945 | if ( out.n[0].text == null ) { 1946 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 1947 | str += "" + out.o[n] + oSpace[n] + ""; 1948 | } 1949 | } 1950 | 1951 | for ( i = 0; i < out.n.length; i++ ) { 1952 | if (out.n[i].text == null) { 1953 | str += "" + out.n[i] + nSpace[i] + ""; 1954 | } 1955 | else { 1956 | // `pre` initialized at top of scope 1957 | pre = ""; 1958 | 1959 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 1960 | pre += "" + out.o[n] + oSpace[n] + ""; 1961 | } 1962 | str += " " + out.n[i].text + nSpace[i] + pre; 1963 | } 1964 | } 1965 | } 1966 | 1967 | return str; 1968 | }; 1969 | }()); 1970 | 1971 | // for CommonJS enviroments, export everything 1972 | if ( typeof exports !== "undefined" ) { 1973 | extend(exports, QUnit); 1974 | } 1975 | 1976 | // get at whatever the global object is, like window in browsers 1977 | }( (function() {return this;}.call()) )); 1978 | -------------------------------------------------------------------------------- /test/js/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Qconfig Javascript Unit Tests 6 | 7 | 8 | 9 | 10 | 371 |
    372 | 373 | 374 | 375 | 495 | 496 | --------------------------------------------------------------------------------