├── .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 | 
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 |
19 |
20 |
21 |
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 | "" +
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 += "Expected: | " + expected + " |
";
805 |
806 | if ( actual != expected ) {
807 | output += "Result: | " + actual + " |
";
808 | output += "Diff: | " + QUnit.diff( expected, actual ) + " |
";
809 | }
810 |
811 | source = sourceFromStacktrace();
812 |
813 | if ( source ) {
814 | details.source = source;
815 | output += "Source: | " + escapeInnerText( source ) + " |
";
816 | }
817 |
818 | output += "
";
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 += "Result: | " + escapeInnerText( actual ) + " |
";
850 | }
851 |
852 | if ( source ) {
853 | details.source = source;
854 | output += "Source: | " + escapeInnerText( source ) + " |
";
855 | }
856 |
857 | output += "
";
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 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
263 |
264 |
370 |
371 |
372 |
373 |
374 |
375 |
495 |
496 |
--------------------------------------------------------------------------------