' ).attr( 'class', 'dialog-header' ).text( i18n.t( 'chooseEntity', { type: type } ) ).append(
133 | $( '' ).attr( {
134 | href: '#',
135 | class: 'close',
136 | title: i18n.t( 'cancel' )
137 | } ).append(
138 | $( '
' ).attr( {
139 | src: 'res/close.png',
140 | alt: i18n.t( 'cancel' ),
141 | width: 20,
142 | height: 20
143 | } )
144 | )
145 | .click( function() {
146 | $.modal.close();
147 | return false;
148 | } )
149 | )
150 | );
151 | /* jshint -W083 */
152 | for ( var i in entities ) {
153 | $( '', { class: 'entity-select' } )
154 | .append( $( '', { class: 'label' } ).text( entities[i].label ) )
155 | .append( $( '
' ) )
156 | .append( $( '', { class: 'description' } ).text( entities[i].description ) )
157 | .append( $( '
' ) )
158 | .append( $( '', { class: 'aliases' } ).text( entities[i].aliases ? i18n.t( 'alsoKnown' ) + entities[i].aliases.join( ', ' ) : '' ) )
159 | .click( { id: entities[i].id }, function( e ) {
160 | deferred.resolve( e.data.id );
161 | $.modal.close();
162 | } )
163 | .appendTo( $dialog );
164 | }
165 | /* jshint +W083 */
166 | $dialog.modal( {
167 | maxWidth: 700,
168 | onClose: function() {
169 | deferred.reject();
170 | $.modal.close();
171 | }
172 | } );
173 | return deferred.promise();
174 | }
175 |
176 | /**
177 | * Combines the given values.
178 | */
179 | function combineValues( values ) {
180 | var value = '';
181 | for ( var i in values ) {
182 | if ( value !== '' && i == values.length - 1 ) {
183 | value += ' ' + i18n.t( 'and' ) + ' ';
184 | }
185 | else if ( i > 0 ) {
186 | value += ', ';
187 | }
188 | value += values[i];
189 | }
190 | return value;
191 | }
192 |
193 | /**
194 | * Handles the questions.
195 | *
196 | * @param {string} question
197 | */
198 | function handleQuestion( question ) {
199 | // parse the question
200 | parser.parseQuestion( question )
201 | .then( function( parsed ) {
202 | handleParsed( parsed );
203 | }, function() {
204 | // the question could not be parsed
205 | showError( i18n.t( 'unparsable' ) );
206 | } );
207 | }
208 |
209 | /**
210 | * Builds the links for the details.
211 | *
212 | * @param {string} link target/label
213 | */
214 | function linkToWD( target, label ) {
215 | return '' + (label||target) + '';
216 | }
217 |
218 | /**
219 | * Handles the parsed parts of the question.
220 | *
221 | * @param {object} parsed
222 | */
223 | function handleParsed( parsed ) {
224 | // search the item
225 | ask.searchEntity( 'item', parsed.item )
226 | .then( function( itemId ) {
227 | showDetails( 'Item: ' + linkToWD( itemId ) );
228 | // question after a specific property => must be queried
229 | if ( parsed.property ) {
230 | // search the property
231 | ask.searchEntity( 'property', parsed.property )
232 | .then( function( propertyId ) {
233 | showDetails( ', Property: ' + linkToWD( 'Property:' + propertyId, propertyId ) );
234 | // get the claims
235 | return ask.getDatavalues( itemId, propertyId );
236 | }, function( notfound ) {
237 | // property not found
238 | if ( notfound !== false ) {
239 | showError( i18n.t( 'propertynotfound', { property: parsed.property } ) );
240 | } else {
241 | showIntro();
242 | }
243 | return false;
244 | } )
245 | .then( function( values ) {
246 | // format the values
247 | return ask.formatDatavalues( values );
248 | }, function( state ) {
249 | if ( state !== false ) {
250 | showError( i18n.t( 'nodata', { subject: parsed.article + ' ' + parsed.property + ' ' + parsed.possesive + ' ' + parsed.item } ) );
251 | }
252 | return false;
253 | } )
254 | .then( function( formattedValues ) {
255 | // display the values
256 | parsed.value = combineValues( formattedValues );
257 | console.log( parsed );
258 | var format = parsed.callback ? parsed.callback : '$article $property $possesive $item $verb $value.';
259 | var answer = parser.buildAnswer( format, parsed );
260 | showResult( answer );
261 | }, function( state ) {
262 | // formatting failed
263 | if ( state !== false ) {
264 | showError( i18n.t( 'notformatted' ) );
265 | }
266 | return false;
267 | } );
268 | }
269 | // common question => get the description
270 | else {
271 | api.getEntities( itemId, 'descriptions' )
272 | .done( function( data ) {
273 | if ( data.entities[itemId].descriptions[api.language()] ) {
274 | showResult( data.entities[itemId].descriptions[api.language()].value );
275 | } else {
276 | showResult( itemId );
277 | }
278 | } );
279 | }
280 | }, function( notfound ) {
281 | // item not found
282 | if ( notfound !== false ) {
283 | showError( i18n.t( 'itemnotfound', { item: parsed.item } ) );
284 | } else {
285 | showIntro();
286 | }
287 | } );
288 | }
289 |
290 | } )( jQuery );
291 |
--------------------------------------------------------------------------------
/src/parser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JavaScript api to parse questions optimized to query data from Wikibase
3 | * Dependencies:
4 | * # jQuery < http://jquery.com/ >
5 | * # XRegExp < http://xregexp.com/ >
6 | *
7 | * @author Bene
8 | * @license GNU GPL v2+
9 | * @version 0.1
10 | */
11 | ( function( $, regex, ns ) {
12 | 'use strict';
13 |
14 | /**
15 | * Contains the regexes used for parsing.
16 | *
17 | * @var {object}
18 | */
19 | var regexes = {};
20 |
21 | /**
22 | * Contains shortcut attributes that can be used in the reges.
23 | *
24 | * @var {object}
25 | */
26 | var attributes = {};
27 |
28 | /**
29 | * Constructor to create a new parser object.
30 | *
31 | * @param {string} language
32 | *
33 | * @constructor
34 | */
35 | ns.Parser = function( language ) {
36 | this._language = language;
37 | this._initPatterns();
38 | };
39 |
40 | $.extend( ns.Parser.prototype, {
41 |
42 | /**
43 | * Sets or gets the language.
44 | *
45 | * @param {string} language
46 | */
47 | language: function( language ) {
48 | if ( language ) {
49 | this._language = language;
50 | this._initPatterns();
51 | }
52 | return this._language;
53 | },
54 |
55 | /**
56 | * Loads the patterns.
57 | */
58 | _initPatterns: function() {
59 | this._promise = $.getJSON( 'patterns/' + this._language + '.json', function( data ) {
60 | regexes = data.regexes;
61 | attributes = data.attributes;
62 | } );
63 | },
64 |
65 | /**
66 | * Builds an answer string based on the given format string and the given attributes.
67 | *
68 | * @param {string} format
69 | * @param {object} attributes
70 | * @return {string}
71 | */
72 | buildAnswer: function( format, attributes ) {
73 | for ( var i in attributes ) {
74 | format = format.replace( new RegExp( '\\$' + i, 'g' ), attributes[i] );
75 | }
76 | //format = format.replace( /undefined /g, '' );
77 | format = format.replace( /\$[\w\d-_\|]+ ?/g, '' );
78 | format = format.charAt( 0 ).toUpperCase() + format.slice( 1 ); // ucfirst
79 | return format;
80 | },
81 |
82 | /**
83 | * Parses the question and returns the an object containing some of the following keys:
84 | * [ 'question', 'verb', 'article', 'property', 'possesive', 'item' ]
85 | *
86 | * @param {string} question
87 | * @return {object}
88 | */
89 | parseQuestion: function( question ) {
90 | var deferred = $.Deferred();
91 | this._promise.then( function() {
92 | question = question.trim();
93 | if ( question.indexOf( '?', question.length - 1 ) !== -1 ) {
94 | question = question.substring( 0, question.length - 1 );
95 | }
96 | for ( var r in regexes ) {
97 | var regString = regexes[r].regex;
98 | for ( var a in attributes ) {
99 | regString = regString.replace( new RegExp( '\\$' + a, 'g' ), attributes[a] );
100 | }
101 | var reg = regex( '^' + regString + '$', 'i' );
102 | if ( reg.test( question ) ) {
103 | var parts = regex.exec( question, reg );
104 | var result = $.extend( {}, regexes[r], parts );
105 | deferred.resolve( result );
106 | return;
107 | }
108 | }
109 | deferred.reject();
110 | } );
111 | return deferred.promise();
112 | }
113 | } );
114 |
115 | } )( jQuery, XRegExp, window );
--------------------------------------------------------------------------------
/test/qunit-1.13.0.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * QUnit 1.13.0
3 | * http://qunitjs.com/
4 | *
5 | * Copyright 2013 jQuery Foundation and other contributors
6 | * Released under the MIT license
7 | * http://jquery.org/license
8 | *
9 | * Date: 2014-01-04T17:09Z
10 | */
11 |
12 | /** Font Family and Sizes */
13 |
14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
16 | }
17 |
18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
19 | #qunit-tests { font-size: smaller; }
20 |
21 |
22 | /** Resets */
23 |
24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
25 | margin: 0;
26 | padding: 0;
27 | }
28 |
29 |
30 | /** Header */
31 |
32 | #qunit-header {
33 | padding: 0.5em 0 0.5em 1em;
34 |
35 | color: #8699a4;
36 | background-color: #0d3349;
37 |
38 | font-size: 1.5em;
39 | line-height: 1em;
40 | font-weight: normal;
41 |
42 | border-radius: 5px 5px 0 0;
43 | -moz-border-radius: 5px 5px 0 0;
44 | -webkit-border-top-right-radius: 5px;
45 | -webkit-border-top-left-radius: 5px;
46 | }
47 |
48 | #qunit-header a {
49 | text-decoration: none;
50 | color: #c2ccd1;
51 | }
52 |
53 | #qunit-header a:hover,
54 | #qunit-header a:focus {
55 | color: #fff;
56 | }
57 |
58 | #qunit-testrunner-toolbar label {
59 | display: inline-block;
60 | padding: 0 .5em 0 .1em;
61 | }
62 |
63 | #qunit-banner {
64 | height: 5px;
65 | }
66 |
67 | #qunit-testrunner-toolbar {
68 | padding: 0.5em 0 0.5em 2em;
69 | color: #5E740B;
70 | background-color: #eee;
71 | overflow: hidden;
72 | }
73 |
74 | #qunit-userAgent {
75 | padding: 0.5em 0 0.5em 2.5em;
76 | background-color: #2b81af;
77 | color: #fff;
78 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
79 | }
80 |
81 | #qunit-modulefilter-container {
82 | float: right;
83 | }
84 |
85 | /** Tests: Pass/Fail */
86 |
87 | #qunit-tests {
88 | list-style-position: inside;
89 | }
90 |
91 | #qunit-tests li {
92 | padding: 0.4em 0.5em 0.4em 2.5em;
93 | border-bottom: 1px solid #fff;
94 | list-style-position: inside;
95 | }
96 |
97 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
98 | display: none;
99 | }
100 |
101 | #qunit-tests li strong {
102 | cursor: pointer;
103 | }
104 |
105 | #qunit-tests li a {
106 | padding: 0.5em;
107 | color: #c2ccd1;
108 | text-decoration: none;
109 | }
110 | #qunit-tests li a:hover,
111 | #qunit-tests li a:focus {
112 | color: #000;
113 | }
114 |
115 | #qunit-tests li .runtime {
116 | float: right;
117 | font-size: smaller;
118 | }
119 |
120 | .qunit-assert-list {
121 | margin-top: 0.5em;
122 | padding: 0.5em;
123 |
124 | background-color: #fff;
125 |
126 | border-radius: 5px;
127 | -moz-border-radius: 5px;
128 | -webkit-border-radius: 5px;
129 | }
130 |
131 | .qunit-collapsed {
132 | display: none;
133 | }
134 |
135 | #qunit-tests table {
136 | border-collapse: collapse;
137 | margin-top: .2em;
138 | }
139 |
140 | #qunit-tests th {
141 | text-align: right;
142 | vertical-align: top;
143 | padding: 0 .5em 0 0;
144 | }
145 |
146 | #qunit-tests td {
147 | vertical-align: top;
148 | }
149 |
150 | #qunit-tests pre {
151 | margin: 0;
152 | white-space: pre-wrap;
153 | word-wrap: break-word;
154 | }
155 |
156 | #qunit-tests del {
157 | background-color: #e0f2be;
158 | color: #374e0c;
159 | text-decoration: none;
160 | }
161 |
162 | #qunit-tests ins {
163 | background-color: #ffcaca;
164 | color: #500;
165 | text-decoration: none;
166 | }
167 |
168 | /*** Test Counts */
169 |
170 | #qunit-tests b.counts { color: black; }
171 | #qunit-tests b.passed { color: #5E740B; }
172 | #qunit-tests b.failed { color: #710909; }
173 |
174 | #qunit-tests li li {
175 | padding: 5px;
176 | background-color: #fff;
177 | border-bottom: none;
178 | list-style-position: inside;
179 | }
180 |
181 | /*** Passing Styles */
182 |
183 | #qunit-tests li li.pass {
184 | color: #3c510c;
185 | background-color: #fff;
186 | border-left: 10px solid #C6E746;
187 | }
188 |
189 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
190 | #qunit-tests .pass .test-name { color: #366097; }
191 |
192 | #qunit-tests .pass .test-actual,
193 | #qunit-tests .pass .test-expected { color: #999999; }
194 |
195 | #qunit-banner.qunit-pass { background-color: #C6E746; }
196 |
197 | /*** Failing Styles */
198 |
199 | #qunit-tests li li.fail {
200 | color: #710909;
201 | background-color: #fff;
202 | border-left: 10px solid #EE5757;
203 | white-space: pre;
204 | }
205 |
206 | #qunit-tests > li:last-child {
207 | border-radius: 0 0 5px 5px;
208 | -moz-border-radius: 0 0 5px 5px;
209 | -webkit-border-bottom-right-radius: 5px;
210 | -webkit-border-bottom-left-radius: 5px;
211 | }
212 |
213 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
214 | #qunit-tests .fail .test-name,
215 | #qunit-tests .fail .module-name { color: #000000; }
216 |
217 | #qunit-tests .fail .test-actual { color: #EE5757; }
218 | #qunit-tests .fail .test-expected { color: green; }
219 |
220 | #qunit-banner.qunit-fail { background-color: #EE5757; }
221 |
222 |
223 | /** Result */
224 |
225 | #qunit-testresult {
226 | padding: 0.5em 0.5em 0.5em 2.5em;
227 |
228 | color: #2b81af;
229 | background-color: #D2E0E6;
230 |
231 | border-bottom: 1px solid white;
232 | }
233 | #qunit-testresult .module-name {
234 | font-weight: bold;
235 | }
236 |
237 | /** Fixture */
238 |
239 | #qunit-fixture {
240 | position: absolute;
241 | top: -10000px;
242 | left: -10000px;
243 | width: 1000px;
244 | height: 1000px;
245 | }
246 |
--------------------------------------------------------------------------------
/test/qunit-1.13.0.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * QUnit 1.13.0
3 | * http://qunitjs.com/
4 | *
5 | * Copyright 2013 jQuery Foundation and other contributors
6 | * Released under the MIT license
7 | * http://jquery.org/license
8 | *
9 | * Date: 2014-01-04T17:09Z
10 | */
11 |
12 | (function( window ) {
13 |
14 | var QUnit,
15 | assert,
16 | config,
17 | onErrorFnPrev,
18 | testId = 0,
19 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""),
20 | toString = Object.prototype.toString,
21 | hasOwn = Object.prototype.hasOwnProperty,
22 | // Keep a local reference to Date (GH-283)
23 | Date = window.Date,
24 | setTimeout = window.setTimeout,
25 | defined = {
26 | document: typeof window.document !== "undefined",
27 | setTimeout: typeof window.setTimeout !== "undefined",
28 | sessionStorage: (function() {
29 | var x = "qunit-test-string";
30 | try {
31 | sessionStorage.setItem( x, x );
32 | sessionStorage.removeItem( x );
33 | return true;
34 | } catch( e ) {
35 | return false;
36 | }
37 | }())
38 | },
39 | /**
40 | * Provides a normalized error string, correcting an issue
41 | * with IE 7 (and prior) where Error.prototype.toString is
42 | * not properly implemented
43 | *
44 | * Based on http://es5.github.com/#x15.11.4.4
45 | *
46 | * @param {String|Error} error
47 | * @return {String} error message
48 | */
49 | errorString = function( error ) {
50 | var name, message,
51 | errorString = error.toString();
52 | if ( errorString.substring( 0, 7 ) === "[object" ) {
53 | name = error.name ? error.name.toString() : "Error";
54 | message = error.message ? error.message.toString() : "";
55 | if ( name && message ) {
56 | return name + ": " + message;
57 | } else if ( name ) {
58 | return name;
59 | } else if ( message ) {
60 | return message;
61 | } else {
62 | return "Error";
63 | }
64 | } else {
65 | return errorString;
66 | }
67 | },
68 | /**
69 | * Makes a clone of an object using only Array or Object as base,
70 | * and copies over the own enumerable properties.
71 | *
72 | * @param {Object} obj
73 | * @return {Object} New object with only the own properties (recursively).
74 | */
75 | objectValues = function( obj ) {
76 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392.
77 | /*jshint newcap: false */
78 | var key, val,
79 | vals = QUnit.is( "array", obj ) ? [] : {};
80 | for ( key in obj ) {
81 | if ( hasOwn.call( obj, key ) ) {
82 | val = obj[key];
83 | vals[key] = val === Object(val) ? objectValues(val) : val;
84 | }
85 | }
86 | return vals;
87 | };
88 |
89 |
90 | // Root QUnit object.
91 | // `QUnit` initialized at top of scope
92 | QUnit = {
93 |
94 | // call on start of module test to prepend name to all tests
95 | module: function( name, testEnvironment ) {
96 | config.currentModule = name;
97 | config.currentModuleTestEnvironment = testEnvironment;
98 | config.modules[name] = true;
99 | },
100 |
101 | asyncTest: function( testName, expected, callback ) {
102 | if ( arguments.length === 2 ) {
103 | callback = expected;
104 | expected = null;
105 | }
106 |
107 | QUnit.test( testName, expected, callback, true );
108 | },
109 |
110 | test: function( testName, expected, callback, async ) {
111 | var test,
112 | nameHtml = "" + escapeText( testName ) + "";
113 |
114 | if ( arguments.length === 2 ) {
115 | callback = expected;
116 | expected = null;
117 | }
118 |
119 | if ( config.currentModule ) {
120 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml;
121 | }
122 |
123 | test = new Test({
124 | nameHtml: nameHtml,
125 | testName: testName,
126 | expected: expected,
127 | async: async,
128 | callback: callback,
129 | module: config.currentModule,
130 | moduleTestEnvironment: config.currentModuleTestEnvironment,
131 | stack: sourceFromStacktrace( 2 )
132 | });
133 |
134 | if ( !validTest( test ) ) {
135 | return;
136 | }
137 |
138 | test.queue();
139 | },
140 |
141 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through.
142 | expect: function( asserts ) {
143 | if (arguments.length === 1) {
144 | config.current.expected = asserts;
145 | } else {
146 | return config.current.expected;
147 | }
148 | },
149 |
150 | start: function( count ) {
151 | // QUnit hasn't been initialized yet.
152 | // Note: RequireJS (et al) may delay onLoad
153 | if ( config.semaphore === undefined ) {
154 | QUnit.begin(function() {
155 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first
156 | setTimeout(function() {
157 | QUnit.start( count );
158 | });
159 | });
160 | return;
161 | }
162 |
163 | config.semaphore -= count || 1;
164 | // don't start until equal number of stop-calls
165 | if ( config.semaphore > 0 ) {
166 | return;
167 | }
168 | // ignore if start is called more often then stop
169 | if ( config.semaphore < 0 ) {
170 | config.semaphore = 0;
171 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) );
172 | return;
173 | }
174 | // A slight delay, to avoid any current callbacks
175 | if ( defined.setTimeout ) {
176 | setTimeout(function() {
177 | if ( config.semaphore > 0 ) {
178 | return;
179 | }
180 | if ( config.timeout ) {
181 | clearTimeout( config.timeout );
182 | }
183 |
184 | config.blocking = false;
185 | process( true );
186 | }, 13);
187 | } else {
188 | config.blocking = false;
189 | process( true );
190 | }
191 | },
192 |
193 | stop: function( count ) {
194 | config.semaphore += count || 1;
195 | config.blocking = true;
196 |
197 | if ( config.testTimeout && defined.setTimeout ) {
198 | clearTimeout( config.timeout );
199 | config.timeout = setTimeout(function() {
200 | QUnit.ok( false, "Test timed out" );
201 | config.semaphore = 1;
202 | QUnit.start();
203 | }, config.testTimeout );
204 | }
205 | }
206 | };
207 |
208 | // We use the prototype to distinguish between properties that should
209 | // be exposed as globals (and in exports) and those that shouldn't
210 | (function() {
211 | function F() {}
212 | F.prototype = QUnit;
213 | QUnit = new F();
214 | // Make F QUnit's constructor so that we can add to the prototype later
215 | QUnit.constructor = F;
216 | }());
217 |
218 | /**
219 | * Config object: Maintain internal state
220 | * Later exposed as QUnit.config
221 | * `config` initialized at top of scope
222 | */
223 | config = {
224 | // The queue of tests to run
225 | queue: [],
226 |
227 | // block until document ready
228 | blocking: true,
229 |
230 | // when enabled, show only failing tests
231 | // gets persisted through sessionStorage and can be changed in UI via checkbox
232 | hidepassed: false,
233 |
234 | // by default, run previously failed tests first
235 | // very useful in combination with "Hide passed tests" checked
236 | reorder: true,
237 |
238 | // by default, modify document.title when suite is done
239 | altertitle: true,
240 |
241 | // when enabled, all tests must call expect()
242 | requireExpects: false,
243 |
244 | // add checkboxes that are persisted in the query-string
245 | // when enabled, the id is set to `true` as a `QUnit.config` property
246 | urlConfig: [
247 | {
248 | id: "noglobals",
249 | label: "Check for Globals",
250 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings."
251 | },
252 | {
253 | id: "notrycatch",
254 | label: "No try-catch",
255 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings."
256 | }
257 | ],
258 |
259 | // Set of all modules.
260 | modules: {},
261 |
262 | // logging callback queues
263 | begin: [],
264 | done: [],
265 | log: [],
266 | testStart: [],
267 | testDone: [],
268 | moduleStart: [],
269 | moduleDone: []
270 | };
271 |
272 | // Initialize more QUnit.config and QUnit.urlParams
273 | (function() {
274 | var i,
275 | location = window.location || { search: "", protocol: "file:" },
276 | params = location.search.slice( 1 ).split( "&" ),
277 | length = params.length,
278 | urlParams = {},
279 | current;
280 |
281 | if ( params[ 0 ] ) {
282 | for ( i = 0; i < length; i++ ) {
283 | current = params[ i ].split( "=" );
284 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
285 | // allow just a key to turn on a flag, e.g., test.html?noglobals
286 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
287 | urlParams[ current[ 0 ] ] = current[ 1 ];
288 | }
289 | }
290 |
291 | QUnit.urlParams = urlParams;
292 |
293 | // String search anywhere in moduleName+testName
294 | config.filter = urlParams.filter;
295 |
296 | // Exact match of the module name
297 | config.module = urlParams.module;
298 |
299 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null;
300 |
301 | // Figure out if we're running the tests from a server or not
302 | QUnit.isLocal = location.protocol === "file:";
303 | }());
304 |
305 | extend( QUnit, {
306 |
307 | config: config,
308 |
309 | // Initialize the configuration options
310 | init: function() {
311 | extend( config, {
312 | stats: { all: 0, bad: 0 },
313 | moduleStats: { all: 0, bad: 0 },
314 | started: +new Date(),
315 | updateRate: 1000,
316 | blocking: false,
317 | autostart: true,
318 | autorun: false,
319 | filter: "",
320 | queue: [],
321 | semaphore: 1
322 | });
323 |
324 | var tests, banner, result,
325 | qunit = id( "qunit" );
326 |
327 | if ( qunit ) {
328 | qunit.innerHTML =
329 | "" + escapeText( document.title ) + "
" +
330 | "" +
331 | "" +
332 | "" +
333 | "
";
334 | }
335 |
336 | tests = id( "qunit-tests" );
337 | banner = id( "qunit-banner" );
338 | result = id( "qunit-testresult" );
339 |
340 | if ( tests ) {
341 | tests.innerHTML = "";
342 | }
343 |
344 | if ( banner ) {
345 | banner.className = "";
346 | }
347 |
348 | if ( result ) {
349 | result.parentNode.removeChild( result );
350 | }
351 |
352 | if ( tests ) {
353 | result = document.createElement( "p" );
354 | result.id = "qunit-testresult";
355 | result.className = "result";
356 | tests.parentNode.insertBefore( result, tests );
357 | result.innerHTML = "Running...
";
358 | }
359 | },
360 |
361 | // Resets the test setup. Useful for tests that modify the DOM.
362 | /*
363 | DEPRECATED: Use multiple tests instead of resetting inside a test.
364 | Use testStart or testDone for custom cleanup.
365 | This method will throw an error in 2.0, and will be removed in 2.1
366 | */
367 | reset: function() {
368 | var fixture = id( "qunit-fixture" );
369 | if ( fixture ) {
370 | fixture.innerHTML = config.fixture;
371 | }
372 | },
373 |
374 | // Safe object type checking
375 | is: function( type, obj ) {
376 | return QUnit.objectType( obj ) === type;
377 | },
378 |
379 | objectType: function( obj ) {
380 | if ( typeof obj === "undefined" ) {
381 | return "undefined";
382 | }
383 |
384 | // Consider: typeof null === object
385 | if ( obj === null ) {
386 | return "null";
387 | }
388 |
389 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/),
390 | type = match && match[1] || "";
391 |
392 | switch ( type ) {
393 | case "Number":
394 | if ( isNaN(obj) ) {
395 | return "nan";
396 | }
397 | return "number";
398 | case "String":
399 | case "Boolean":
400 | case "Array":
401 | case "Date":
402 | case "RegExp":
403 | case "Function":
404 | return type.toLowerCase();
405 | }
406 | if ( typeof obj === "object" ) {
407 | return "object";
408 | }
409 | return undefined;
410 | },
411 |
412 | push: function( result, actual, expected, message ) {
413 | if ( !config.current ) {
414 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() );
415 | }
416 |
417 | var output, source,
418 | details = {
419 | module: config.current.module,
420 | name: config.current.testName,
421 | result: result,
422 | message: message,
423 | actual: actual,
424 | expected: expected
425 | };
426 |
427 | message = escapeText( message ) || ( result ? "okay" : "failed" );
428 | message = " ";
429 | output = message;
430 |
431 | if ( !result ) {
432 | expected = escapeText( QUnit.jsDump.parse(expected) );
433 | actual = escapeText( QUnit.jsDump.parse(actual) );
434 | output += "Expected: " + expected + "
";
435 |
436 | if ( actual !== expected ) {
437 | output += "Result: " + actual + "
";
438 | output += "Diff: " + QUnit.diff( expected, actual ) + "
";
439 | }
440 |
441 | source = sourceFromStacktrace();
442 |
443 | if ( source ) {
444 | details.source = source;
445 | output += "Source: " + escapeText( source ) + "
";
446 | }
447 |
448 | output += "
";
449 | }
450 |
451 | runLoggingCallbacks( "log", QUnit, details );
452 |
453 | config.current.assertions.push({
454 | result: !!result,
455 | message: output
456 | });
457 | },
458 |
459 | pushFailure: function( message, source, actual ) {
460 | if ( !config.current ) {
461 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) );
462 | }
463 |
464 | var output,
465 | details = {
466 | module: config.current.module,
467 | name: config.current.testName,
468 | result: false,
469 | message: message
470 | };
471 |
472 | message = escapeText( message ) || "error";
473 | message = " ";
474 | output = message;
475 |
476 | output += "";
477 |
478 | if ( actual ) {
479 | output += "Result: " + escapeText( actual ) + "
";
480 | }
481 |
482 | if ( source ) {
483 | details.source = source;
484 | output += "Source: " + escapeText( source ) + "
";
485 | }
486 |
487 | output += "
";
488 |
489 | runLoggingCallbacks( "log", QUnit, details );
490 |
491 | config.current.assertions.push({
492 | result: false,
493 | message: output
494 | });
495 | },
496 |
497 | url: function( params ) {
498 | params = extend( extend( {}, QUnit.urlParams ), params );
499 | var key,
500 | querystring = "?";
501 |
502 | for ( key in params ) {
503 | if ( hasOwn.call( params, key ) ) {
504 | querystring += encodeURIComponent( key ) + "=" +
505 | encodeURIComponent( params[ key ] ) + "&";
506 | }
507 | }
508 | return window.location.protocol + "//" + window.location.host +
509 | window.location.pathname + querystring.slice( 0, -1 );
510 | },
511 |
512 | extend: extend,
513 | id: id,
514 | addEvent: addEvent,
515 | addClass: addClass,
516 | hasClass: hasClass,
517 | removeClass: removeClass
518 | // load, equiv, jsDump, diff: Attached later
519 | });
520 |
521 | /**
522 | * @deprecated: Created for backwards compatibility with test runner that set the hook function
523 | * into QUnit.{hook}, instead of invoking it and passing the hook function.
524 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here.
525 | * Doing this allows us to tell if the following methods have been overwritten on the actual
526 | * QUnit object.
527 | */
528 | extend( QUnit.constructor.prototype, {
529 |
530 | // Logging callbacks; all receive a single argument with the listed properties
531 | // run test/logs.html for any related changes
532 | begin: registerLoggingCallback( "begin" ),
533 |
534 | // done: { failed, passed, total, runtime }
535 | done: registerLoggingCallback( "done" ),
536 |
537 | // log: { result, actual, expected, message }
538 | log: registerLoggingCallback( "log" ),
539 |
540 | // testStart: { name }
541 | testStart: registerLoggingCallback( "testStart" ),
542 |
543 | // testDone: { name, failed, passed, total, runtime }
544 | testDone: registerLoggingCallback( "testDone" ),
545 |
546 | // moduleStart: { name }
547 | moduleStart: registerLoggingCallback( "moduleStart" ),
548 |
549 | // moduleDone: { name, failed, passed, total }
550 | moduleDone: registerLoggingCallback( "moduleDone" )
551 | });
552 |
553 | if ( !defined.document || document.readyState === "complete" ) {
554 | config.autorun = true;
555 | }
556 |
557 | QUnit.load = function() {
558 | runLoggingCallbacks( "begin", QUnit, {} );
559 |
560 | // Initialize the config, saving the execution queue
561 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val,
562 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter,
563 | numModules = 0,
564 | moduleNames = [],
565 | moduleFilterHtml = "",
566 | urlConfigHtml = "",
567 | oldconfig = extend( {}, config );
568 |
569 | QUnit.init();
570 | extend(config, oldconfig);
571 |
572 | config.blocking = false;
573 |
574 | len = config.urlConfig.length;
575 |
576 | for ( i = 0; i < len; i++ ) {
577 | val = config.urlConfig[i];
578 | if ( typeof val === "string" ) {
579 | val = {
580 | id: val,
581 | label: val,
582 | tooltip: "[no tooltip available]"
583 | };
584 | }
585 | config[ val.id ] = QUnit.urlParams[ val.id ];
586 | urlConfigHtml += "";
592 | }
593 | for ( i in config.modules ) {
594 | if ( config.modules.hasOwnProperty( i ) ) {
595 | moduleNames.push(i);
596 | }
597 | }
598 | numModules = moduleNames.length;
599 | moduleNames.sort( function( a, b ) {
600 | return a.localeCompare( b );
601 | });
602 | moduleFilterHtml += "";
613 |
614 | // `userAgent` initialized at top of scope
615 | userAgent = id( "qunit-userAgent" );
616 | if ( userAgent ) {
617 | userAgent.innerHTML = navigator.userAgent;
618 | }
619 |
620 | // `banner` initialized at top of scope
621 | banner = id( "qunit-header" );
622 | if ( banner ) {
623 | banner.innerHTML = "" + banner.innerHTML + " ";
624 | }
625 |
626 | // `toolbar` initialized at top of scope
627 | toolbar = id( "qunit-testrunner-toolbar" );
628 | if ( toolbar ) {
629 | // `filter` initialized at top of scope
630 | filter = document.createElement( "input" );
631 | filter.type = "checkbox";
632 | filter.id = "qunit-filter-pass";
633 |
634 | addEvent( filter, "click", function() {
635 | var tmp,
636 | ol = id( "qunit-tests" );
637 |
638 | if ( filter.checked ) {
639 | ol.className = ol.className + " hidepass";
640 | } else {
641 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " ";
642 | ol.className = tmp.replace( / hidepass /, " " );
643 | }
644 | if ( defined.sessionStorage ) {
645 | if (filter.checked) {
646 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" );
647 | } else {
648 | sessionStorage.removeItem( "qunit-filter-passed-tests" );
649 | }
650 | }
651 | });
652 |
653 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) {
654 | filter.checked = true;
655 | // `ol` initialized at top of scope
656 | ol = id( "qunit-tests" );
657 | ol.className = ol.className + " hidepass";
658 | }
659 | toolbar.appendChild( filter );
660 |
661 | // `label` initialized at top of scope
662 | label = document.createElement( "label" );
663 | label.setAttribute( "for", "qunit-filter-pass" );
664 | label.setAttribute( "title", "Only show tests and assertions that fail. Stored in sessionStorage." );
665 | label.innerHTML = "Hide passed tests";
666 | toolbar.appendChild( label );
667 |
668 | urlConfigCheckboxesContainer = document.createElement("span");
669 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml;
670 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input");
671 | // For oldIE support:
672 | // * Add handlers to the individual elements instead of the container
673 | // * Use "click" instead of "change"
674 | // * Fallback from event.target to event.srcElement
675 | addEvents( urlConfigCheckboxes, "click", function( event ) {
676 | var params = {},
677 | target = event.target || event.srcElement;
678 | params[ target.name ] = target.checked ? true : undefined;
679 | window.location = QUnit.url( params );
680 | });
681 | toolbar.appendChild( urlConfigCheckboxesContainer );
682 |
683 | if (numModules > 1) {
684 | moduleFilter = document.createElement( "span" );
685 | moduleFilter.setAttribute( "id", "qunit-modulefilter-container" );
686 | moduleFilter.innerHTML = moduleFilterHtml;
687 | addEvent( moduleFilter.lastChild, "change", function() {
688 | var selectBox = moduleFilter.getElementsByTagName("select")[0],
689 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value);
690 |
691 | window.location = QUnit.url({
692 | module: ( selectedModule === "" ) ? undefined : selectedModule,
693 | // Remove any existing filters
694 | filter: undefined,
695 | testNumber: undefined
696 | });
697 | });
698 | toolbar.appendChild(moduleFilter);
699 | }
700 | }
701 |
702 | // `main` initialized at top of scope
703 | main = id( "qunit-fixture" );
704 | if ( main ) {
705 | config.fixture = main.innerHTML;
706 | }
707 |
708 | if ( config.autostart ) {
709 | QUnit.start();
710 | }
711 | };
712 |
713 | if ( defined.document ) {
714 | addEvent( window, "load", QUnit.load );
715 | }
716 |
717 | // `onErrorFnPrev` initialized at top of scope
718 | // Preserve other handlers
719 | onErrorFnPrev = window.onerror;
720 |
721 | // Cover uncaught exceptions
722 | // Returning true will suppress the default browser handler,
723 | // returning false will let it run.
724 | window.onerror = function ( error, filePath, linerNr ) {
725 | var ret = false;
726 | if ( onErrorFnPrev ) {
727 | ret = onErrorFnPrev( error, filePath, linerNr );
728 | }
729 |
730 | // Treat return value as window.onerror itself does,
731 | // Only do our handling if not suppressed.
732 | if ( ret !== true ) {
733 | if ( QUnit.config.current ) {
734 | if ( QUnit.config.current.ignoreGlobalErrors ) {
735 | return true;
736 | }
737 | QUnit.pushFailure( error, filePath + ":" + linerNr );
738 | } else {
739 | QUnit.test( "global failure", extend( function() {
740 | QUnit.pushFailure( error, filePath + ":" + linerNr );
741 | }, { validTest: validTest } ) );
742 | }
743 | return false;
744 | }
745 |
746 | return ret;
747 | };
748 |
749 | function done() {
750 | config.autorun = true;
751 |
752 | // Log the last module results
753 | if ( config.previousModule ) {
754 | runLoggingCallbacks( "moduleDone", QUnit, {
755 | name: config.previousModule,
756 | failed: config.moduleStats.bad,
757 | passed: config.moduleStats.all - config.moduleStats.bad,
758 | total: config.moduleStats.all
759 | });
760 | }
761 | delete config.previousModule;
762 |
763 | var i, key,
764 | banner = id( "qunit-banner" ),
765 | tests = id( "qunit-tests" ),
766 | runtime = +new Date() - config.started,
767 | passed = config.stats.all - config.stats.bad,
768 | html = [
769 | "Tests completed in ",
770 | runtime,
771 | " milliseconds.
",
772 | "",
773 | passed,
774 | " assertions of ",
775 | config.stats.all,
776 | " passed, ",
777 | config.stats.bad,
778 | " failed."
779 | ].join( "" );
780 |
781 | if ( banner ) {
782 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" );
783 | }
784 |
785 | if ( tests ) {
786 | id( "qunit-testresult" ).innerHTML = html;
787 | }
788 |
789 | if ( config.altertitle && defined.document && document.title ) {
790 | // show ✖ for good, ✔ for bad suite result in title
791 | // use escape sequences in case file gets loaded with non-utf-8-charset
792 | document.title = [
793 | ( config.stats.bad ? "\u2716" : "\u2714" ),
794 | document.title.replace( /^[\u2714\u2716] /i, "" )
795 | ].join( " " );
796 | }
797 |
798 | // clear own sessionStorage items if all tests passed
799 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) {
800 | // `key` & `i` initialized at top of scope
801 | for ( i = 0; i < sessionStorage.length; i++ ) {
802 | key = sessionStorage.key( i++ );
803 | if ( key.indexOf( "qunit-test-" ) === 0 ) {
804 | sessionStorage.removeItem( key );
805 | }
806 | }
807 | }
808 |
809 | // scroll back to top to show results
810 | if ( window.scrollTo ) {
811 | window.scrollTo(0, 0);
812 | }
813 |
814 | runLoggingCallbacks( "done", QUnit, {
815 | failed: config.stats.bad,
816 | passed: passed,
817 | total: config.stats.all,
818 | runtime: runtime
819 | });
820 | }
821 |
822 | /** @return Boolean: true if this test should be ran */
823 | function validTest( test ) {
824 | var include,
825 | filter = config.filter && config.filter.toLowerCase(),
826 | module = config.module && config.module.toLowerCase(),
827 | fullName = (test.module + ": " + test.testName).toLowerCase();
828 |
829 | // Internally-generated tests are always valid
830 | if ( test.callback && test.callback.validTest === validTest ) {
831 | delete test.callback.validTest;
832 | return true;
833 | }
834 |
835 | if ( config.testNumber ) {
836 | return test.testNumber === config.testNumber;
837 | }
838 |
839 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) {
840 | return false;
841 | }
842 |
843 | if ( !filter ) {
844 | return true;
845 | }
846 |
847 | include = filter.charAt( 0 ) !== "!";
848 | if ( !include ) {
849 | filter = filter.slice( 1 );
850 | }
851 |
852 | // If the filter matches, we need to honour include
853 | if ( fullName.indexOf( filter ) !== -1 ) {
854 | return include;
855 | }
856 |
857 | // Otherwise, do the opposite
858 | return !include;
859 | }
860 |
861 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions)
862 | // Later Safari and IE10 are supposed to support error.stack as well
863 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
864 | function extractStacktrace( e, offset ) {
865 | offset = offset === undefined ? 3 : offset;
866 |
867 | var stack, include, i;
868 |
869 | if ( e.stacktrace ) {
870 | // Opera
871 | return e.stacktrace.split( "\n" )[ offset + 3 ];
872 | } else if ( e.stack ) {
873 | // Firefox, Chrome
874 | stack = e.stack.split( "\n" );
875 | if (/^error$/i.test( stack[0] ) ) {
876 | stack.shift();
877 | }
878 | if ( fileName ) {
879 | include = [];
880 | for ( i = offset; i < stack.length; i++ ) {
881 | if ( stack[ i ].indexOf( fileName ) !== -1 ) {
882 | break;
883 | }
884 | include.push( stack[ i ] );
885 | }
886 | if ( include.length ) {
887 | return include.join( "\n" );
888 | }
889 | }
890 | return stack[ offset ];
891 | } else if ( e.sourceURL ) {
892 | // Safari, PhantomJS
893 | // hopefully one day Safari provides actual stacktraces
894 | // exclude useless self-reference for generated Error objects
895 | if ( /qunit.js$/.test( e.sourceURL ) ) {
896 | return;
897 | }
898 | // for actual exceptions, this is useful
899 | return e.sourceURL + ":" + e.line;
900 | }
901 | }
902 | function sourceFromStacktrace( offset ) {
903 | try {
904 | throw new Error();
905 | } catch ( e ) {
906 | return extractStacktrace( e, offset );
907 | }
908 | }
909 |
910 | /**
911 | * Escape text for attribute or text content.
912 | */
913 | function escapeText( s ) {
914 | if ( !s ) {
915 | return "";
916 | }
917 | s = s + "";
918 | // Both single quotes and double quotes (for attributes)
919 | return s.replace( /['"<>&]/g, function( s ) {
920 | switch( s ) {
921 | case "'":
922 | return "'";
923 | case "\"":
924 | return """;
925 | case "<":
926 | return "<";
927 | case ">":
928 | return ">";
929 | case "&":
930 | return "&";
931 | }
932 | });
933 | }
934 |
935 | function synchronize( callback, last ) {
936 | config.queue.push( callback );
937 |
938 | if ( config.autorun && !config.blocking ) {
939 | process( last );
940 | }
941 | }
942 |
943 | function process( last ) {
944 | function next() {
945 | process( last );
946 | }
947 | var start = new Date().getTime();
948 | config.depth = config.depth ? config.depth + 1 : 1;
949 |
950 | while ( config.queue.length && !config.blocking ) {
951 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) {
952 | config.queue.shift()();
953 | } else {
954 | setTimeout( next, 13 );
955 | break;
956 | }
957 | }
958 | config.depth--;
959 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
960 | done();
961 | }
962 | }
963 |
964 | function saveGlobal() {
965 | config.pollution = [];
966 |
967 | if ( config.noglobals ) {
968 | for ( var key in window ) {
969 | if ( hasOwn.call( window, key ) ) {
970 | // in Opera sometimes DOM element ids show up here, ignore them
971 | if ( /^qunit-test-output/.test( key ) ) {
972 | continue;
973 | }
974 | config.pollution.push( key );
975 | }
976 | }
977 | }
978 | }
979 |
980 | function checkPollution() {
981 | var newGlobals,
982 | deletedGlobals,
983 | old = config.pollution;
984 |
985 | saveGlobal();
986 |
987 | newGlobals = diff( config.pollution, old );
988 | if ( newGlobals.length > 0 ) {
989 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") );
990 | }
991 |
992 | deletedGlobals = diff( old, config.pollution );
993 | if ( deletedGlobals.length > 0 ) {
994 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") );
995 | }
996 | }
997 |
998 | // returns a new Array with the elements that are in a but not in b
999 | function diff( a, b ) {
1000 | var i, j,
1001 | result = a.slice();
1002 |
1003 | for ( i = 0; i < result.length; i++ ) {
1004 | for ( j = 0; j < b.length; j++ ) {
1005 | if ( result[i] === b[j] ) {
1006 | result.splice( i, 1 );
1007 | i--;
1008 | break;
1009 | }
1010 | }
1011 | }
1012 | return result;
1013 | }
1014 |
1015 | function extend( a, b ) {
1016 | for ( var prop in b ) {
1017 | if ( hasOwn.call( b, prop ) ) {
1018 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor
1019 | if ( !( prop === "constructor" && a === window ) ) {
1020 | if ( b[ prop ] === undefined ) {
1021 | delete a[ prop ];
1022 | } else {
1023 | a[ prop ] = b[ prop ];
1024 | }
1025 | }
1026 | }
1027 | }
1028 |
1029 | return a;
1030 | }
1031 |
1032 | /**
1033 | * @param {HTMLElement} elem
1034 | * @param {string} type
1035 | * @param {Function} fn
1036 | */
1037 | function addEvent( elem, type, fn ) {
1038 | if ( elem.addEventListener ) {
1039 |
1040 | // Standards-based browsers
1041 | elem.addEventListener( type, fn, false );
1042 | } else if ( elem.attachEvent ) {
1043 |
1044 | // support: IE <9
1045 | elem.attachEvent( "on" + type, fn );
1046 | } else {
1047 |
1048 | // Caller must ensure support for event listeners is present
1049 | throw new Error( "addEvent() was called in a context without event listener support" );
1050 | }
1051 | }
1052 |
1053 | /**
1054 | * @param {Array|NodeList} elems
1055 | * @param {string} type
1056 | * @param {Function} fn
1057 | */
1058 | function addEvents( elems, type, fn ) {
1059 | var i = elems.length;
1060 | while ( i-- ) {
1061 | addEvent( elems[i], type, fn );
1062 | }
1063 | }
1064 |
1065 | function hasClass( elem, name ) {
1066 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1;
1067 | }
1068 |
1069 | function addClass( elem, name ) {
1070 | if ( !hasClass( elem, name ) ) {
1071 | elem.className += (elem.className ? " " : "") + name;
1072 | }
1073 | }
1074 |
1075 | function removeClass( elem, name ) {
1076 | var set = " " + elem.className + " ";
1077 | // Class name may appear multiple times
1078 | while ( set.indexOf(" " + name + " ") > -1 ) {
1079 | set = set.replace(" " + name + " " , " ");
1080 | }
1081 | // If possible, trim it for prettiness, but not necessarily
1082 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, "");
1083 | }
1084 |
1085 | function id( name ) {
1086 | return defined.document && document.getElementById && document.getElementById( name );
1087 | }
1088 |
1089 | function registerLoggingCallback( key ) {
1090 | return function( callback ) {
1091 | config[key].push( callback );
1092 | };
1093 | }
1094 |
1095 | // Supports deprecated method of completely overwriting logging callbacks
1096 | function runLoggingCallbacks( key, scope, args ) {
1097 | var i, callbacks;
1098 | if ( QUnit.hasOwnProperty( key ) ) {
1099 | QUnit[ key ].call(scope, args );
1100 | } else {
1101 | callbacks = config[ key ];
1102 | for ( i = 0; i < callbacks.length; i++ ) {
1103 | callbacks[ i ].call( scope, args );
1104 | }
1105 | }
1106 | }
1107 |
1108 | // from jquery.js
1109 | function inArray( elem, array ) {
1110 | if ( array.indexOf ) {
1111 | return array.indexOf( elem );
1112 | }
1113 |
1114 | for ( var i = 0, length = array.length; i < length; i++ ) {
1115 | if ( array[ i ] === elem ) {
1116 | return i;
1117 | }
1118 | }
1119 |
1120 | return -1;
1121 | }
1122 |
1123 | function Test( settings ) {
1124 | extend( this, settings );
1125 | this.assertions = [];
1126 | this.testNumber = ++Test.count;
1127 | }
1128 |
1129 | Test.count = 0;
1130 |
1131 | Test.prototype = {
1132 | init: function() {
1133 | var a, b, li,
1134 | tests = id( "qunit-tests" );
1135 |
1136 | if ( tests ) {
1137 | b = document.createElement( "strong" );
1138 | b.innerHTML = this.nameHtml;
1139 |
1140 | // `a` initialized at top of scope
1141 | a = document.createElement( "a" );
1142 | a.innerHTML = "Rerun";
1143 | a.href = QUnit.url({ testNumber: this.testNumber });
1144 |
1145 | li = document.createElement( "li" );
1146 | li.appendChild( b );
1147 | li.appendChild( a );
1148 | li.className = "running";
1149 | li.id = this.id = "qunit-test-output" + testId++;
1150 |
1151 | tests.appendChild( li );
1152 | }
1153 | },
1154 | setup: function() {
1155 | if (
1156 | // Emit moduleStart when we're switching from one module to another
1157 | this.module !== config.previousModule ||
1158 | // They could be equal (both undefined) but if the previousModule property doesn't
1159 | // yet exist it means this is the first test in a suite that isn't wrapped in a
1160 | // module, in which case we'll just emit a moduleStart event for 'undefined'.
1161 | // Without this, reporters can get testStart before moduleStart which is a problem.
1162 | !hasOwn.call( config, "previousModule" )
1163 | ) {
1164 | if ( hasOwn.call( config, "previousModule" ) ) {
1165 | runLoggingCallbacks( "moduleDone", QUnit, {
1166 | name: config.previousModule,
1167 | failed: config.moduleStats.bad,
1168 | passed: config.moduleStats.all - config.moduleStats.bad,
1169 | total: config.moduleStats.all
1170 | });
1171 | }
1172 | config.previousModule = this.module;
1173 | config.moduleStats = { all: 0, bad: 0 };
1174 | runLoggingCallbacks( "moduleStart", QUnit, {
1175 | name: this.module
1176 | });
1177 | }
1178 |
1179 | config.current = this;
1180 |
1181 | this.testEnvironment = extend({
1182 | setup: function() {},
1183 | teardown: function() {}
1184 | }, this.moduleTestEnvironment );
1185 |
1186 | this.started = +new Date();
1187 | runLoggingCallbacks( "testStart", QUnit, {
1188 | name: this.testName,
1189 | module: this.module
1190 | });
1191 |
1192 | /*jshint camelcase:false */
1193 |
1194 |
1195 | /**
1196 | * Expose the current test environment.
1197 | *
1198 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead.
1199 | */
1200 | QUnit.current_testEnvironment = this.testEnvironment;
1201 |
1202 | /*jshint camelcase:true */
1203 |
1204 | if ( !config.pollution ) {
1205 | saveGlobal();
1206 | }
1207 | if ( config.notrycatch ) {
1208 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
1209 | return;
1210 | }
1211 | try {
1212 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert );
1213 | } catch( e ) {
1214 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
1215 | }
1216 | },
1217 | run: function() {
1218 | config.current = this;
1219 |
1220 | var running = id( "qunit-testresult" );
1221 |
1222 | if ( running ) {
1223 | running.innerHTML = "Running:
" + this.nameHtml;
1224 | }
1225 |
1226 | if ( this.async ) {
1227 | QUnit.stop();
1228 | }
1229 |
1230 | this.callbackStarted = +new Date();
1231 |
1232 | if ( config.notrycatch ) {
1233 | this.callback.call( this.testEnvironment, QUnit.assert );
1234 | this.callbackRuntime = +new Date() - this.callbackStarted;
1235 | return;
1236 | }
1237 |
1238 | try {
1239 | this.callback.call( this.testEnvironment, QUnit.assert );
1240 | this.callbackRuntime = +new Date() - this.callbackStarted;
1241 | } catch( e ) {
1242 | this.callbackRuntime = +new Date() - this.callbackStarted;
1243 |
1244 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
1245 | // else next test will carry the responsibility
1246 | saveGlobal();
1247 |
1248 | // Restart the tests if they're blocking
1249 | if ( config.blocking ) {
1250 | QUnit.start();
1251 | }
1252 | }
1253 | },
1254 | teardown: function() {
1255 | config.current = this;
1256 | if ( config.notrycatch ) {
1257 | if ( typeof this.callbackRuntime === "undefined" ) {
1258 | this.callbackRuntime = +new Date() - this.callbackStarted;
1259 | }
1260 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
1261 | return;
1262 | } else {
1263 | try {
1264 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert );
1265 | } catch( e ) {
1266 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) );
1267 | }
1268 | }
1269 | checkPollution();
1270 | },
1271 | finish: function() {
1272 | config.current = this;
1273 | if ( config.requireExpects && this.expected === null ) {
1274 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack );
1275 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
1276 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack );
1277 | } else if ( this.expected === null && !this.assertions.length ) {
1278 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack );
1279 | }
1280 |
1281 | var i, assertion, a, b, time, li, ol,
1282 | test = this,
1283 | good = 0,
1284 | bad = 0,
1285 | tests = id( "qunit-tests" );
1286 |
1287 | this.runtime = +new Date() - this.started;
1288 | config.stats.all += this.assertions.length;
1289 | config.moduleStats.all += this.assertions.length;
1290 |
1291 | if ( tests ) {
1292 | ol = document.createElement( "ol" );
1293 | ol.className = "qunit-assert-list";
1294 |
1295 | for ( i = 0; i < this.assertions.length; i++ ) {
1296 | assertion = this.assertions[i];
1297 |
1298 | li = document.createElement( "li" );
1299 | li.className = assertion.result ? "pass" : "fail";
1300 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" );
1301 | ol.appendChild( li );
1302 |
1303 | if ( assertion.result ) {
1304 | good++;
1305 | } else {
1306 | bad++;
1307 | config.stats.bad++;
1308 | config.moduleStats.bad++;
1309 | }
1310 | }
1311 |
1312 | // store result when possible
1313 | if ( QUnit.config.reorder && defined.sessionStorage ) {
1314 | if ( bad ) {
1315 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad );
1316 | } else {
1317 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName );
1318 | }
1319 | }
1320 |
1321 | if ( bad === 0 ) {
1322 | addClass( ol, "qunit-collapsed" );
1323 | }
1324 |
1325 | // `b` initialized at top of scope
1326 | b = document.createElement( "strong" );
1327 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")";
1328 |
1329 | addEvent(b, "click", function() {
1330 | var next = b.parentNode.lastChild,
1331 | collapsed = hasClass( next, "qunit-collapsed" );
1332 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" );
1333 | });
1334 |
1335 | addEvent(b, "dblclick", function( e ) {
1336 | var target = e && e.target ? e.target : window.event.srcElement;
1337 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) {
1338 | target = target.parentNode;
1339 | }
1340 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) {
1341 | window.location = QUnit.url({ testNumber: test.testNumber });
1342 | }
1343 | });
1344 |
1345 | // `time` initialized at top of scope
1346 | time = document.createElement( "span" );
1347 | time.className = "runtime";
1348 | time.innerHTML = this.runtime + " ms";
1349 |
1350 | // `li` initialized at top of scope
1351 | li = id( this.id );
1352 | li.className = bad ? "fail" : "pass";
1353 | li.removeChild( li.firstChild );
1354 | a = li.firstChild;
1355 | li.appendChild( b );
1356 | li.appendChild( a );
1357 | li.appendChild( time );
1358 | li.appendChild( ol );
1359 |
1360 | } else {
1361 | for ( i = 0; i < this.assertions.length; i++ ) {
1362 | if ( !this.assertions[i].result ) {
1363 | bad++;
1364 | config.stats.bad++;
1365 | config.moduleStats.bad++;
1366 | }
1367 | }
1368 | }
1369 |
1370 | runLoggingCallbacks( "testDone", QUnit, {
1371 | name: this.testName,
1372 | module: this.module,
1373 | failed: bad,
1374 | passed: this.assertions.length - bad,
1375 | total: this.assertions.length,
1376 | runtime: this.runtime,
1377 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
1378 | duration: this.runtime,
1379 | });
1380 |
1381 | QUnit.reset();
1382 |
1383 | config.current = undefined;
1384 | },
1385 |
1386 | queue: function() {
1387 | var bad,
1388 | test = this;
1389 |
1390 | synchronize(function() {
1391 | test.init();
1392 | });
1393 | function run() {
1394 | // each of these can by async
1395 | synchronize(function() {
1396 | test.setup();
1397 | });
1398 | synchronize(function() {
1399 | test.run();
1400 | });
1401 | synchronize(function() {
1402 | test.teardown();
1403 | });
1404 | synchronize(function() {
1405 | test.finish();
1406 | });
1407 | }
1408 |
1409 | // `bad` initialized at top of scope
1410 | // defer when previous test run passed, if storage is available
1411 | bad = QUnit.config.reorder && defined.sessionStorage &&
1412 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName );
1413 |
1414 | if ( bad ) {
1415 | run();
1416 | } else {
1417 | synchronize( run, true );
1418 | }
1419 | }
1420 | };
1421 |
1422 | // `assert` initialized at top of scope
1423 | // Assert helpers
1424 | // All of these must either call QUnit.push() or manually do:
1425 | // - runLoggingCallbacks( "log", .. );
1426 | // - config.current.assertions.push({ .. });
1427 | assert = QUnit.assert = {
1428 | /**
1429 | * Asserts rough true-ish result.
1430 | * @name ok
1431 | * @function
1432 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
1433 | */
1434 | ok: function( result, msg ) {
1435 | if ( !config.current ) {
1436 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) );
1437 | }
1438 | result = !!result;
1439 | msg = msg || ( result ? "okay" : "failed" );
1440 |
1441 | var source,
1442 | details = {
1443 | module: config.current.module,
1444 | name: config.current.testName,
1445 | result: result,
1446 | message: msg
1447 | };
1448 |
1449 | msg = " ";
1450 |
1451 | if ( !result ) {
1452 | source = sourceFromStacktrace( 2 );
1453 | if ( source ) {
1454 | details.source = source;
1455 | msg += "Source: " +
1456 | escapeText( source ) +
1457 | "
";
1458 | }
1459 | }
1460 | runLoggingCallbacks( "log", QUnit, details );
1461 | config.current.assertions.push({
1462 | result: result,
1463 | message: msg
1464 | });
1465 | },
1466 |
1467 | /**
1468 | * Assert that the first two arguments are equal, with an optional message.
1469 | * Prints out both actual and expected values.
1470 | * @name equal
1471 | * @function
1472 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" );
1473 | */
1474 | equal: function( actual, expected, message ) {
1475 | /*jshint eqeqeq:false */
1476 | QUnit.push( expected == actual, actual, expected, message );
1477 | },
1478 |
1479 | /**
1480 | * @name notEqual
1481 | * @function
1482 | */
1483 | notEqual: function( actual, expected, message ) {
1484 | /*jshint eqeqeq:false */
1485 | QUnit.push( expected != actual, actual, expected, message );
1486 | },
1487 |
1488 | /**
1489 | * @name propEqual
1490 | * @function
1491 | */
1492 | propEqual: function( actual, expected, message ) {
1493 | actual = objectValues(actual);
1494 | expected = objectValues(expected);
1495 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
1496 | },
1497 |
1498 | /**
1499 | * @name notPropEqual
1500 | * @function
1501 | */
1502 | notPropEqual: function( actual, expected, message ) {
1503 | actual = objectValues(actual);
1504 | expected = objectValues(expected);
1505 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
1506 | },
1507 |
1508 | /**
1509 | * @name deepEqual
1510 | * @function
1511 | */
1512 | deepEqual: function( actual, expected, message ) {
1513 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
1514 | },
1515 |
1516 | /**
1517 | * @name notDeepEqual
1518 | * @function
1519 | */
1520 | notDeepEqual: function( actual, expected, message ) {
1521 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message );
1522 | },
1523 |
1524 | /**
1525 | * @name strictEqual
1526 | * @function
1527 | */
1528 | strictEqual: function( actual, expected, message ) {
1529 | QUnit.push( expected === actual, actual, expected, message );
1530 | },
1531 |
1532 | /**
1533 | * @name notStrictEqual
1534 | * @function
1535 | */
1536 | notStrictEqual: function( actual, expected, message ) {
1537 | QUnit.push( expected !== actual, actual, expected, message );
1538 | },
1539 |
1540 | "throws": function( block, expected, message ) {
1541 | var actual,
1542 | expectedOutput = expected,
1543 | ok = false;
1544 |
1545 | // 'expected' is optional
1546 | if ( typeof expected === "string" ) {
1547 | message = expected;
1548 | expected = null;
1549 | }
1550 |
1551 | config.current.ignoreGlobalErrors = true;
1552 | try {
1553 | block.call( config.current.testEnvironment );
1554 | } catch (e) {
1555 | actual = e;
1556 | }
1557 | config.current.ignoreGlobalErrors = false;
1558 |
1559 | if ( actual ) {
1560 | // we don't want to validate thrown error
1561 | if ( !expected ) {
1562 | ok = true;
1563 | expectedOutput = null;
1564 | // expected is a regexp
1565 | } else if ( QUnit.objectType( expected ) === "regexp" ) {
1566 | ok = expected.test( errorString( actual ) );
1567 | // expected is a constructor
1568 | } else if ( actual instanceof expected ) {
1569 | ok = true;
1570 | // expected is a validation function which returns true is validation passed
1571 | } else if ( expected.call( {}, actual ) === true ) {
1572 | expectedOutput = null;
1573 | ok = true;
1574 | }
1575 |
1576 | QUnit.push( ok, actual, expectedOutput, message );
1577 | } else {
1578 | QUnit.pushFailure( message, null, "No exception was thrown." );
1579 | }
1580 | }
1581 | };
1582 |
1583 | /**
1584 | * @deprecated since 1.8.0
1585 | * Kept assertion helpers in root for backwards compatibility.
1586 | */
1587 | extend( QUnit.constructor.prototype, assert );
1588 |
1589 | /**
1590 | * @deprecated since 1.9.0
1591 | * Kept to avoid TypeErrors for undefined methods.
1592 | */
1593 | QUnit.constructor.prototype.raises = function() {
1594 | QUnit.push( false, false, false, "QUnit.raises has been deprecated since 2012 (fad3c1ea), use QUnit.throws instead" );
1595 | };
1596 |
1597 | /**
1598 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0
1599 | * Kept to avoid TypeErrors for undefined methods.
1600 | */
1601 | QUnit.constructor.prototype.equals = function() {
1602 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" );
1603 | };
1604 | QUnit.constructor.prototype.same = function() {
1605 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" );
1606 | };
1607 |
1608 | // Test for equality any JavaScript type.
1609 | // Author: Philippe Rathé
1610 | QUnit.equiv = (function() {
1611 |
1612 | // Call the o related callback with the given arguments.
1613 | function bindCallbacks( o, callbacks, args ) {
1614 | var prop = QUnit.objectType( o );
1615 | if ( prop ) {
1616 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) {
1617 | return callbacks[ prop ].apply( callbacks, args );
1618 | } else {
1619 | return callbacks[ prop ]; // or undefined
1620 | }
1621 | }
1622 | }
1623 |
1624 | // the real equiv function
1625 | var innerEquiv,
1626 | // stack to decide between skip/abort functions
1627 | callers = [],
1628 | // stack to avoiding loops from circular referencing
1629 | parents = [],
1630 | parentsB = [],
1631 |
1632 | getProto = Object.getPrototypeOf || function ( obj ) {
1633 | /*jshint camelcase:false */
1634 | return obj.__proto__;
1635 | },
1636 | callbacks = (function () {
1637 |
1638 | // for string, boolean, number and null
1639 | function useStrictEquality( b, a ) {
1640 | /*jshint eqeqeq:false */
1641 | if ( b instanceof a.constructor || a instanceof b.constructor ) {
1642 | // to catch short annotation VS 'new' annotation of a
1643 | // declaration
1644 | // e.g. var i = 1;
1645 | // var j = new Number(1);
1646 | return a == b;
1647 | } else {
1648 | return a === b;
1649 | }
1650 | }
1651 |
1652 | return {
1653 | "string": useStrictEquality,
1654 | "boolean": useStrictEquality,
1655 | "number": useStrictEquality,
1656 | "null": useStrictEquality,
1657 | "undefined": useStrictEquality,
1658 |
1659 | "nan": function( b ) {
1660 | return isNaN( b );
1661 | },
1662 |
1663 | "date": function( b, a ) {
1664 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf();
1665 | },
1666 |
1667 | "regexp": function( b, a ) {
1668 | return QUnit.objectType( b ) === "regexp" &&
1669 | // the regex itself
1670 | a.source === b.source &&
1671 | // and its modifiers
1672 | a.global === b.global &&
1673 | // (gmi) ...
1674 | a.ignoreCase === b.ignoreCase &&
1675 | a.multiline === b.multiline &&
1676 | a.sticky === b.sticky;
1677 | },
1678 |
1679 | // - skip when the property is a method of an instance (OOP)
1680 | // - abort otherwise,
1681 | // initial === would have catch identical references anyway
1682 | "function": function() {
1683 | var caller = callers[callers.length - 1];
1684 | return caller !== Object && typeof caller !== "undefined";
1685 | },
1686 |
1687 | "array": function( b, a ) {
1688 | var i, j, len, loop, aCircular, bCircular;
1689 |
1690 | // b could be an object literal here
1691 | if ( QUnit.objectType( b ) !== "array" ) {
1692 | return false;
1693 | }
1694 |
1695 | len = a.length;
1696 | if ( len !== b.length ) {
1697 | // safe and faster
1698 | return false;
1699 | }
1700 |
1701 | // track reference to avoid circular references
1702 | parents.push( a );
1703 | parentsB.push( b );
1704 | for ( i = 0; i < len; i++ ) {
1705 | loop = false;
1706 | for ( j = 0; j < parents.length; j++ ) {
1707 | aCircular = parents[j] === a[i];
1708 | bCircular = parentsB[j] === b[i];
1709 | if ( aCircular || bCircular ) {
1710 | if ( a[i] === b[i] || aCircular && bCircular ) {
1711 | loop = true;
1712 | } else {
1713 | parents.pop();
1714 | parentsB.pop();
1715 | return false;
1716 | }
1717 | }
1718 | }
1719 | if ( !loop && !innerEquiv(a[i], b[i]) ) {
1720 | parents.pop();
1721 | parentsB.pop();
1722 | return false;
1723 | }
1724 | }
1725 | parents.pop();
1726 | parentsB.pop();
1727 | return true;
1728 | },
1729 |
1730 | "object": function( b, a ) {
1731 | /*jshint forin:false */
1732 | var i, j, loop, aCircular, bCircular,
1733 | // Default to true
1734 | eq = true,
1735 | aProperties = [],
1736 | bProperties = [];
1737 |
1738 | // comparing constructors is more strict than using
1739 | // instanceof
1740 | if ( a.constructor !== b.constructor ) {
1741 | // Allow objects with no prototype to be equivalent to
1742 | // objects with Object as their constructor.
1743 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
1744 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
1745 | return false;
1746 | }
1747 | }
1748 |
1749 | // stack constructor before traversing properties
1750 | callers.push( a.constructor );
1751 |
1752 | // track reference to avoid circular references
1753 | parents.push( a );
1754 | parentsB.push( b );
1755 |
1756 | // be strict: don't ensure hasOwnProperty and go deep
1757 | for ( i in a ) {
1758 | loop = false;
1759 | for ( j = 0; j < parents.length; j++ ) {
1760 | aCircular = parents[j] === a[i];
1761 | bCircular = parentsB[j] === b[i];
1762 | if ( aCircular || bCircular ) {
1763 | if ( a[i] === b[i] || aCircular && bCircular ) {
1764 | loop = true;
1765 | } else {
1766 | eq = false;
1767 | break;
1768 | }
1769 | }
1770 | }
1771 | aProperties.push(i);
1772 | if ( !loop && !innerEquiv(a[i], b[i]) ) {
1773 | eq = false;
1774 | break;
1775 | }
1776 | }
1777 |
1778 | parents.pop();
1779 | parentsB.pop();
1780 | callers.pop(); // unstack, we are done
1781 |
1782 | for ( i in b ) {
1783 | bProperties.push( i ); // collect b's properties
1784 | }
1785 |
1786 | // Ensures identical properties name
1787 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
1788 | }
1789 | };
1790 | }());
1791 |
1792 | innerEquiv = function() { // can take multiple arguments
1793 | var args = [].slice.apply( arguments );
1794 | if ( args.length < 2 ) {
1795 | return true; // end transition
1796 | }
1797 |
1798 | return (function( a, b ) {
1799 | if ( a === b ) {
1800 | return true; // catch the most you can
1801 | } else if ( a === null || b === null || typeof a === "undefined" ||
1802 | typeof b === "undefined" ||
1803 | QUnit.objectType(a) !== QUnit.objectType(b) ) {
1804 | return false; // don't lose time with error prone cases
1805 | } else {
1806 | return bindCallbacks(a, callbacks, [ b, a ]);
1807 | }
1808 |
1809 | // apply transition with (1..n) arguments
1810 | }( args[0], args[1] ) && innerEquiv.apply( this, args.splice(1, args.length - 1 )) );
1811 | };
1812 |
1813 | return innerEquiv;
1814 | }());
1815 |
1816 | /**
1817 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com |
1818 | * http://flesler.blogspot.com Licensed under BSD
1819 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008
1820 | *
1821 | * @projectDescription Advanced and extensible data dumping for Javascript.
1822 | * @version 1.0.0
1823 | * @author Ariel Flesler
1824 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
1825 | */
1826 | QUnit.jsDump = (function() {
1827 | function quote( str ) {
1828 | return "\"" + str.toString().replace( /"/g, "\\\"" ) + "\"";
1829 | }
1830 | function literal( o ) {
1831 | return o + "";
1832 | }
1833 | function join( pre, arr, post ) {
1834 | var s = jsDump.separator(),
1835 | base = jsDump.indent(),
1836 | inner = jsDump.indent(1);
1837 | if ( arr.join ) {
1838 | arr = arr.join( "," + s + inner );
1839 | }
1840 | if ( !arr ) {
1841 | return pre + post;
1842 | }
1843 | return [ pre, inner + arr, base + post ].join(s);
1844 | }
1845 | function array( arr, stack ) {
1846 | var i = arr.length, ret = new Array(i);
1847 | this.up();
1848 | while ( i-- ) {
1849 | ret[i] = this.parse( arr[i] , undefined , stack);
1850 | }
1851 | this.down();
1852 | return join( "[", ret, "]" );
1853 | }
1854 |
1855 | var reName = /^function (\w+)/,
1856 | jsDump = {
1857 | // type is used mostly internally, you can fix a (custom)type in advance
1858 | parse: function( obj, type, stack ) {
1859 | stack = stack || [ ];
1860 | var inStack, res,
1861 | parser = this.parsers[ type || this.typeOf(obj) ];
1862 |
1863 | type = typeof parser;
1864 | inStack = inArray( obj, stack );
1865 |
1866 | if ( inStack !== -1 ) {
1867 | return "recursion(" + (inStack - stack.length) + ")";
1868 | }
1869 | if ( type === "function" ) {
1870 | stack.push( obj );
1871 | res = parser.call( this, obj, stack );
1872 | stack.pop();
1873 | return res;
1874 | }
1875 | return ( type === "string" ) ? parser : this.parsers.error;
1876 | },
1877 | typeOf: function( obj ) {
1878 | var type;
1879 | if ( obj === null ) {
1880 | type = "null";
1881 | } else if ( typeof obj === "undefined" ) {
1882 | type = "undefined";
1883 | } else if ( QUnit.is( "regexp", obj) ) {
1884 | type = "regexp";
1885 | } else if ( QUnit.is( "date", obj) ) {
1886 | type = "date";
1887 | } else if ( QUnit.is( "function", obj) ) {
1888 | type = "function";
1889 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) {
1890 | type = "window";
1891 | } else if ( obj.nodeType === 9 ) {
1892 | type = "document";
1893 | } else if ( obj.nodeType ) {
1894 | type = "node";
1895 | } else if (
1896 | // native arrays
1897 | toString.call( obj ) === "[object Array]" ||
1898 | // NodeList objects
1899 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) )
1900 | ) {
1901 | type = "array";
1902 | } else if ( obj.constructor === Error.prototype.constructor ) {
1903 | type = "error";
1904 | } else {
1905 | type = typeof obj;
1906 | }
1907 | return type;
1908 | },
1909 | separator: function() {
1910 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " ";
1911 | },
1912 | // extra can be a number, shortcut for increasing-calling-decreasing
1913 | indent: function( extra ) {
1914 | if ( !this.multiline ) {
1915 | return "";
1916 | }
1917 | var chr = this.indentChar;
1918 | if ( this.HTML ) {
1919 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
1920 | }
1921 | return new Array( this.depth + ( extra || 0 ) ).join(chr);
1922 | },
1923 | up: function( a ) {
1924 | this.depth += a || 1;
1925 | },
1926 | down: function( a ) {
1927 | this.depth -= a || 1;
1928 | },
1929 | setParser: function( name, parser ) {
1930 | this.parsers[name] = parser;
1931 | },
1932 | // The next 3 are exposed so you can use them
1933 | quote: quote,
1934 | literal: literal,
1935 | join: join,
1936 | //
1937 | depth: 1,
1938 | // This is the list of parsers, to modify them, use jsDump.setParser
1939 | parsers: {
1940 | window: "[Window]",
1941 | document: "[Document]",
1942 | error: function(error) {
1943 | return "Error(\"" + error.message + "\")";
1944 | },
1945 | unknown: "[Unknown]",
1946 | "null": "null",
1947 | "undefined": "undefined",
1948 | "function": function( fn ) {
1949 | var ret = "function",
1950 | // functions never have name in IE
1951 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1];
1952 |
1953 | if ( name ) {
1954 | ret += " " + name;
1955 | }
1956 | ret += "( ";
1957 |
1958 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" );
1959 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" );
1960 | },
1961 | array: array,
1962 | nodelist: array,
1963 | "arguments": array,
1964 | object: function( map, stack ) {
1965 | /*jshint forin:false */
1966 | var ret = [ ], keys, key, val, i;
1967 | QUnit.jsDump.up();
1968 | keys = [];
1969 | for ( key in map ) {
1970 | keys.push( key );
1971 | }
1972 | keys.sort();
1973 | for ( i = 0; i < keys.length; i++ ) {
1974 | key = keys[ i ];
1975 | val = map[ key ];
1976 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) );
1977 | }
1978 | QUnit.jsDump.down();
1979 | return join( "{", ret, "}" );
1980 | },
1981 | node: function( node ) {
1982 | var len, i, val,
1983 | open = QUnit.jsDump.HTML ? "<" : "<",
1984 | close = QUnit.jsDump.HTML ? ">" : ">",
1985 | tag = node.nodeName.toLowerCase(),
1986 | ret = open + tag,
1987 | attrs = node.attributes;
1988 |
1989 | if ( attrs ) {
1990 | for ( i = 0, len = attrs.length; i < len; i++ ) {
1991 | val = attrs[i].nodeValue;
1992 | // IE6 includes all attributes in .attributes, even ones not explicitly set.
1993 | // Those have values like undefined, null, 0, false, "" or "inherit".
1994 | if ( val && val !== "inherit" ) {
1995 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" );
1996 | }
1997 | }
1998 | }
1999 | ret += close;
2000 |
2001 | // Show content of TextNode or CDATASection
2002 | if ( node.nodeType === 3 || node.nodeType === 4 ) {
2003 | ret += node.nodeValue;
2004 | }
2005 |
2006 | return ret + open + "/" + tag + close;
2007 | },
2008 | // function calls it internally, it's the arguments part of the function
2009 | functionArgs: function( fn ) {
2010 | var args,
2011 | l = fn.length;
2012 |
2013 | if ( !l ) {
2014 | return "";
2015 | }
2016 |
2017 | args = new Array(l);
2018 | while ( l-- ) {
2019 | // 97 is 'a'
2020 | args[l] = String.fromCharCode(97+l);
2021 | }
2022 | return " " + args.join( ", " ) + " ";
2023 | },
2024 | // object calls it internally, the key part of an item in a map
2025 | key: quote,
2026 | // function calls it internally, it's the content of the function
2027 | functionCode: "[code]",
2028 | // node calls it internally, it's an html attribute value
2029 | attribute: quote,
2030 | string: quote,
2031 | date: quote,
2032 | regexp: literal,
2033 | number: literal,
2034 | "boolean": literal
2035 | },
2036 | // if true, entities are escaped ( <, >, \t, space and \n )
2037 | HTML: false,
2038 | // indentation unit
2039 | indentChar: " ",
2040 | // if true, items in a collection, are separated by a \n, else just a space.
2041 | multiline: true
2042 | };
2043 |
2044 | return jsDump;
2045 | }());
2046 |
2047 | /*
2048 | * Javascript Diff Algorithm
2049 | * By John Resig (http://ejohn.org/)
2050 | * Modified by Chu Alan "sprite"
2051 | *
2052 | * Released under the MIT license.
2053 | *
2054 | * More Info:
2055 | * http://ejohn.org/projects/javascript-diff-algorithm/
2056 | *
2057 | * Usage: QUnit.diff(expected, actual)
2058 | *
2059 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over"
2060 | */
2061 | QUnit.diff = (function() {
2062 | /*jshint eqeqeq:false, eqnull:true */
2063 | function diff( o, n ) {
2064 | var i,
2065 | ns = {},
2066 | os = {};
2067 |
2068 | for ( i = 0; i < n.length; i++ ) {
2069 | if ( !hasOwn.call( ns, n[i] ) ) {
2070 | ns[ n[i] ] = {
2071 | rows: [],
2072 | o: null
2073 | };
2074 | }
2075 | ns[ n[i] ].rows.push( i );
2076 | }
2077 |
2078 | for ( i = 0; i < o.length; i++ ) {
2079 | if ( !hasOwn.call( os, o[i] ) ) {
2080 | os[ o[i] ] = {
2081 | rows: [],
2082 | n: null
2083 | };
2084 | }
2085 | os[ o[i] ].rows.push( i );
2086 | }
2087 |
2088 | for ( i in ns ) {
2089 | if ( hasOwn.call( ns, i ) ) {
2090 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) {
2091 | n[ ns[i].rows[0] ] = {
2092 | text: n[ ns[i].rows[0] ],
2093 | row: os[i].rows[0]
2094 | };
2095 | o[ os[i].rows[0] ] = {
2096 | text: o[ os[i].rows[0] ],
2097 | row: ns[i].rows[0]
2098 | };
2099 | }
2100 | }
2101 | }
2102 |
2103 | for ( i = 0; i < n.length - 1; i++ ) {
2104 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
2105 | n[ i + 1 ] == o[ n[i].row + 1 ] ) {
2106 |
2107 | n[ i + 1 ] = {
2108 | text: n[ i + 1 ],
2109 | row: n[i].row + 1
2110 | };
2111 | o[ n[i].row + 1 ] = {
2112 | text: o[ n[i].row + 1 ],
2113 | row: i + 1
2114 | };
2115 | }
2116 | }
2117 |
2118 | for ( i = n.length - 1; i > 0; i-- ) {
2119 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
2120 | n[ i - 1 ] == o[ n[i].row - 1 ]) {
2121 |
2122 | n[ i - 1 ] = {
2123 | text: n[ i - 1 ],
2124 | row: n[i].row - 1
2125 | };
2126 | o[ n[i].row - 1 ] = {
2127 | text: o[ n[i].row - 1 ],
2128 | row: i - 1
2129 | };
2130 | }
2131 | }
2132 |
2133 | return {
2134 | o: o,
2135 | n: n
2136 | };
2137 | }
2138 |
2139 | return function( o, n ) {
2140 | o = o.replace( /\s+$/, "" );
2141 | n = n.replace( /\s+$/, "" );
2142 |
2143 | var i, pre,
2144 | str = "",
2145 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ),
2146 | oSpace = o.match(/\s+/g),
2147 | nSpace = n.match(/\s+/g);
2148 |
2149 | if ( oSpace == null ) {
2150 | oSpace = [ " " ];
2151 | }
2152 | else {
2153 | oSpace.push( " " );
2154 | }
2155 |
2156 | if ( nSpace == null ) {
2157 | nSpace = [ " " ];
2158 | }
2159 | else {
2160 | nSpace.push( " " );
2161 | }
2162 |
2163 | if ( out.n.length === 0 ) {
2164 | for ( i = 0; i < out.o.length; i++ ) {
2165 | str += "" + out.o[i] + oSpace[i] + "";
2166 | }
2167 | }
2168 | else {
2169 | if ( out.n[0].text == null ) {
2170 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) {
2171 | str += "" + out.o[n] + oSpace[n] + "";
2172 | }
2173 | }
2174 |
2175 | for ( i = 0; i < out.n.length; i++ ) {
2176 | if (out.n[i].text == null) {
2177 | str += "" + out.n[i] + nSpace[i] + "";
2178 | }
2179 | else {
2180 | // `pre` initialized at top of scope
2181 | pre = "";
2182 |
2183 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
2184 | pre += "" + out.o[n] + oSpace[n] + "";
2185 | }
2186 | str += " " + out.n[i].text + nSpace[i] + pre;
2187 | }
2188 | }
2189 | }
2190 |
2191 | return str;
2192 | };
2193 | }());
2194 |
2195 | // For browser, export only select globals
2196 | if ( typeof window !== "undefined" ) {
2197 | extend( window, QUnit.constructor.prototype );
2198 | window.QUnit = QUnit;
2199 | }
2200 |
2201 | // For CommonJS environments, export everything
2202 | if ( typeof module !== "undefined" && module.exports ) {
2203 | module.exports = QUnit;
2204 | }
2205 |
2206 |
2207 | // Get a reference to the global object, like window in browsers
2208 | }( (function() {
2209 | return this;
2210 | })() ));
2211 |
--------------------------------------------------------------------------------
/test/qunit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | QUnit Example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/test/test_api.js:
--------------------------------------------------------------------------------
1 | module( 'api' );
2 |
3 | test( 'constructor', function() {
4 | var api = new Api( 'https://example.com/api.php', 'en' );
5 | equal( api._url, 'https://example.com/api.php', 'url value' );
6 | equal( api._language, 'en', 'language value' );
7 | } );
8 |
9 | test( 'langauge', function() {
10 | var api = new Api( 'https://example.com/api.php', 'en' );
11 | equal( api.language(), 'en', 'get language value' );
12 | equal( api.language( 'de' ), 'de', 'get and set language value' );
13 | equal( api._language, 'de', 'saved language value' );
14 | } );
15 |
16 | asyncTest( '_get', function() {
17 | expect( 1 );
18 |
19 | var api = new Api( 'https://www.wikidata.org/w/api.php', 'en' );
20 | api._get( {
21 | action: 'test'
22 | } )
23 | .done( function( data ) {
24 | equal( data.error.info, 'Unrecognized value for parameter \'action\': test', 'error message' );
25 | } )
26 | .always( start );
27 | } );
28 |
29 | asyncTest( 'format datavalue', function() {
30 | expect( 2 );
31 |
32 | var api = new Api( 'https://www.wikidata.org/w/api.php', 'en' );
33 | var datavalue1 = {"value":{"time":"+00000002001-06-16T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"};
34 | var datavalue2 = {"value":{"entity-type":"item","numeric-id":1208},"type":"wikibase-entityid"};
35 | $.when(
36 | api.formatDatavalue( datavalue1 ),
37 | $.getJSON( 'https://www.wikidata.org/w/api.php?action=wbformatvalue&generate=text%2Fplain&datavalue=' + encodeURIComponent( JSON.stringify( datavalue1 ) ) + '&options=%7B%22lang%22%3A%22en%22%2C%22geoformat%22%3A%22dms%22%7D&format=json&callback=?' ),
38 | api.formatDatavalue( datavalue2 ),
39 | $.getJSON( 'https://www.wikidata.org/w/api.php?action=wbformatvalue&generate=text%2Fplain&datavalue=' + encodeURIComponent( JSON.stringify( datavalue2 ) ) + '&options=%7B%22lang%22%3A%22en%22%2C%22geoformat%22%3A%22dms%22%7D&format=json&callback=?')
40 | )
41 | .then( function( data1, expected1, data2, expected2 ) {
42 | deepEqual(
43 | data1[0],
44 | expected1[0],
45 | 'time data value'
46 | );
47 | deepEqual(
48 | data2[0],
49 | expected2[0],
50 | 'entity id data value'
51 | );
52 | } )
53 | .always( start );
54 | } );
55 |
56 | asyncTest( 'get claims', function() {
57 | expect( 1 );
58 |
59 | var api = new Api( 'https://www.wikidata.org/w/api.php', 'en' );
60 | $.when(
61 | api.getClaims( 'q76', 'p21' ),
62 | $.getJSON( 'https://www.wikidata.org/w/api.php?action=wbgetclaims&entity=q76&property=p21&format=json&callback=?' )
63 | )
64 | .then( function( data1, expected1 ) {
65 | deepEqual(
66 | data1[0],
67 | expected1[0],
68 | 'Checking result'
69 | );
70 | } )
71 | .always( start );
72 | } );
73 |
74 | asyncTest( 'get entities', function() {
75 | expect( 1 );
76 |
77 | var api = new Api( 'https://www.wikidata.org/w/api.php', 'en' );
78 | $.when(
79 | api.getEntities( 'q76', 'labels' ),
80 | $.getJSON( 'https://www.wikidata.org/w/api.php?action=wbgetentities&languages=en&ids=q76&props=labels&format=json&callback=?' )
81 | )
82 | .then( function( data1, expected1 ) {
83 | deepEqual(
84 | data1[0],
85 | expected1[0],
86 | 'Checking result'
87 | );
88 | } )
89 | .always( start );
90 | } );
91 |
92 | asyncTest( 'search entities', function() {
93 | expect( 2 );
94 |
95 | var api = new Api( 'https://www.wikidata.org/w/api.php', 'en' );
96 | $.when(
97 | api.searchEntities( 'item', 'United States' ),
98 | $.getJSON( 'https://www.wikidata.org/w/api.php?action=wbsearchentities&language=en&search=United+States&type=item&format=json&callback=?' ),
99 | api.searchEntities( 'property', 'location' ),
100 | $.getJSON( 'https://www.wikidata.org/w/api.php?action=wbsearchentities&language=en&search=location&type=property&format=json&callback=?' )
101 | )
102 | .then( function( data1, expected1, data2, expected2 ) {
103 | deepEqual(
104 | data1[0],
105 | expected1[0],
106 | 'Checking result'
107 | );
108 | deepEqual(
109 | data2[0],
110 | expected2[0],
111 | 'Checking result'
112 | );
113 | } )
114 | .always( start );
115 | } );
116 |
--------------------------------------------------------------------------------
/test/test_ask.js:
--------------------------------------------------------------------------------
1 | module( 'Ask' );
2 |
3 | test( 'constructor', function() {
4 | var api = new Api( 'https://www.wikidata.org/w/api.php', 'en' );
5 | var ask = new Ask( api );
6 | equal( ask._api, api, 'api value' );
7 | } );
8 |
9 | asyncTest( 'search entity', function() {
10 | expect( 5 );
11 |
12 | var ask = new Ask( new Api( 'https://www.wikidata.org/w/api.php', 'en' ) );
13 | ask.entityChooser( function( type, entities ) {
14 | equal( type, 'item', 'Checking type' );
15 | ok( true, 'Entity choser should be called when the query returns more than one value' );
16 | return $.Deferred().resolve( entities[1].id ).promise();
17 | } );
18 |
19 | $.when( ask.searchEntity( 'item', 'asdfg' ) )
20 | .then( null, function() {
21 | ok( true, 'No item should be found by searching for "asdfg"' );
22 | return ask.searchEntity( 'item', 'Bayerische Motoren Werke' );
23 | } )
24 | .then( function( item ) {
25 | equal( item, 'Q26678', 'One item should be found by searching for "Bayerische Motoren Werke"' );
26 | return ask.searchEntity( 'item', 'BMW' );
27 | } )
28 | .then( function( item ) {
29 | equal( item, 'Q564512', 'Several items should be found by searching for "BMW" and the second one should be choosen' );
30 | } )
31 | .always( start );
32 | } );
33 |
34 | asyncTest( 'get datavalues', function() {
35 | expect( 1 );
36 |
37 | var ask = new Ask( new Api( 'https://www.wikidata.org/w/api.php', 'en' ) );
38 | ask.getDatavalues( 'Q76', 'P21' )
39 | .done( function( values ) {
40 | deepEqual( values, [ {"value":{"entity-type":"item","numeric-id":6581097},"type":"wikibase-entityid"} ], 'Check values' );
41 | } )
42 | .always( start );
43 | } );
44 |
45 | asyncTest( 'format datavalues', function() {
46 | expect( 4 );
47 |
48 | var ask = new Ask( new Api( 'https://www.wikidata.org/w/api.php', 'en' ) );
49 | $.when( ask.formatDatavalues( [] ) )
50 | .then( null, function() {
51 | ok( true, 'If no property values are given it must fail' );
52 | return ask.formatDatavalues( [{"value":{"entity-type":"item","numeric-id":1208},"type":"wikibase-entityid"},{"value":{"entity-type":"item","numeric-id":1201238},"type":"wikibase-entityid"}] );
53 | } )
54 | .then( function( values ) {
55 | // this test fails perhaps because travis does not know the "arguments" feature used in ask.js on line 40. Tips are welcome.
56 | deepEqual( values, [ 'Brandenburg', 'Q1201238' ], 'Check entity id value' );
57 | return ask.formatDatavalues( [{"value":{"time":"+00000002001-06-16T00:00:00Z","timezone":0,"before":0,"after":0,"precision":11,"calendarmodel":"http://www.wikidata.org/entity/Q1985727"},"type":"time"}] );
58 | } )
59 | .then( function( values ) {
60 | deepEqual( values, [ '16 June 2001' ], 'Check time value' );
61 | return ask.formatDatavalues( [{"value":{"latitude":52.516666666667,"longitude":13.383333333333,"altitude":null,"precision":0.016666666666667,"globe":"http://www.wikidata.org/entity/Q2"},"type":"globecoordinate"}] );
62 | } )
63 | .then( function( values ) {
64 | deepEqual( values, [ '52° 31\' 0", 13° 22\' 60"' ], 'Check globecoordinate value' );
65 | } )
66 | .always( start );
67 | } );
--------------------------------------------------------------------------------
/test/test_parser.js:
--------------------------------------------------------------------------------
1 | module( 'Parser' );
2 |
3 | test( 'constructor', function() {
4 | var parser = new Parser( 'en' );
5 | equal( parser._language, 'en', 'language value' );
6 | } );
7 |
8 | test( 'language', function() {
9 | var parser = new Parser( 'en' );
10 | equal( parser.language(), 'en', 'get language value' );
11 | equal( parser.language( 'de' ), 'de', 'get and set language value' );
12 | equal( parser._language, 'de', 'saved language value' );
13 | } );
14 |
15 | test( 'build answer', function() {
16 | var parser = new Parser( 'en' );
17 | var format = '$abc $foo. $hij . $xxx; $bar';
18 | var attributes = {
19 | 'abc': 'def',
20 | 'foo': 'bar',
21 | 'bar': 'foo'
22 | };
23 |
24 | var answer = parser.buildAnswer( format, attributes );
25 | equal( answer, 'Def bar. . ; foo', 'build a proper answer' );
26 | } );
27 |
28 | asyncTest( 'parse question (en)', function() {
29 | expect( 8 );
30 |
31 | var parser = new Parser( 'en' );
32 | var question1 = 'Who is Barack Obama?';
33 | var question2 = 'Who are the presidents of the United States';
34 |
35 | $.when( parser.parseQuestion( question1 ) )
36 | .then( function( parsed1 ) {
37 | equal( parsed1.verb, 'is', 'verb' );
38 | equal( parsed1.item, 'Barack Obama', 'item' );
39 | return parser.parseQuestion( question2 );
40 | } )
41 | .then( function( parsed2 ) {
42 | equal( parsed2.verb, 'are', 'verb' );
43 | equal( parsed2.article, 'the', 'article' );
44 | equal( parsed2.property, 'presidents', 'property' );
45 | equal( parsed2.possesive, 'of the', 'possesive' );
46 | equal( parsed2.item, 'United States', 'item' );
47 | return parser.parseQuestion( 'Foo bar' );
48 | } )
49 | .then( null, function() {
50 | ok( true, 'Invalid value' );
51 | } )
52 | .always( start );
53 | } );
54 |
55 | asyncTest( 'parse question (de)', function() {
56 | expect( 8 );
57 |
58 | var parser = new Parser( 'de' );
59 | var question1 = 'Wer ist Joachim Gauck?';
60 | var question2 = 'Wer sind die Präsidenten der Bundesrepublik Deutschland';
61 |
62 | $.when( parser.parseQuestion( question1 ) )
63 | .then( function( parsed1 ) {
64 | equal( parsed1.verb, 'ist', 'verb' );
65 | equal( parsed1.item, 'Joachim Gauck', 'item' );
66 | return parser.parseQuestion( question2 );
67 | } )
68 | .then( function( parsed2 ) {
69 | equal( parsed2.verb, 'sind', 'verb' );
70 | equal( parsed2.article, 'die', 'article' );
71 | equal( parsed2.property, 'Präsidenten', 'property' );
72 | equal( parsed2.possesive, 'der', 'possesive' );
73 | equal( parsed2.item, 'Bundesrepublik Deutschland', 'item' );
74 | return parser.parseQuestion( 'Foo bar' );
75 | } )
76 | .then( null, function() {
77 | ok( true, 'Invalid value' );
78 | } )
79 | .always( start );
80 | } );
81 |
--------------------------------------------------------------------------------
' ) ) 156 | .append( $( '', { class: 'description' } ).text( entities[i].description ) ) 157 | .append( $( '
' ) ) 158 | .append( $( '', { class: 'aliases' } ).text( entities[i].aliases ? i18n.t( 'alsoKnown' ) + entities[i].aliases.join( ', ' ) : '' ) ) 159 | .click( { id: entities[i].id }, function( e ) { 160 | deferred.resolve( e.data.id ); 161 | $.modal.close(); 162 | } ) 163 | .appendTo( $dialog ); 164 | } 165 | /* jshint +W083 */ 166 | $dialog.modal( { 167 | maxWidth: 700, 168 | onClose: function() { 169 | deferred.reject(); 170 | $.modal.close(); 171 | } 172 | } ); 173 | return deferred.promise(); 174 | } 175 | 176 | /** 177 | * Combines the given values. 178 | */ 179 | function combineValues( values ) { 180 | var value = ''; 181 | for ( var i in values ) { 182 | if ( value !== '' && i == values.length - 1 ) { 183 | value += ' ' + i18n.t( 'and' ) + ' '; 184 | } 185 | else if ( i > 0 ) { 186 | value += ', '; 187 | } 188 | value += values[i]; 189 | } 190 | return value; 191 | } 192 | 193 | /** 194 | * Handles the questions. 195 | * 196 | * @param {string} question 197 | */ 198 | function handleQuestion( question ) { 199 | // parse the question 200 | parser.parseQuestion( question ) 201 | .then( function( parsed ) { 202 | handleParsed( parsed ); 203 | }, function() { 204 | // the question could not be parsed 205 | showError( i18n.t( 'unparsable' ) ); 206 | } ); 207 | } 208 | 209 | /** 210 | * Builds the links for the details. 211 | * 212 | * @param {string} link target/label 213 | */ 214 | function linkToWD( target, label ) { 215 | return '' + (label||target) + ''; 216 | } 217 | 218 | /** 219 | * Handles the parsed parts of the question. 220 | * 221 | * @param {object} parsed 222 | */ 223 | function handleParsed( parsed ) { 224 | // search the item 225 | ask.searchEntity( 'item', parsed.item ) 226 | .then( function( itemId ) { 227 | showDetails( 'Item: ' + linkToWD( itemId ) ); 228 | // question after a specific property => must be queried 229 | if ( parsed.property ) { 230 | // search the property 231 | ask.searchEntity( 'property', parsed.property ) 232 | .then( function( propertyId ) { 233 | showDetails( ', Property: ' + linkToWD( 'Property:' + propertyId, propertyId ) ); 234 | // get the claims 235 | return ask.getDatavalues( itemId, propertyId ); 236 | }, function( notfound ) { 237 | // property not found 238 | if ( notfound !== false ) { 239 | showError( i18n.t( 'propertynotfound', { property: parsed.property } ) ); 240 | } else { 241 | showIntro(); 242 | } 243 | return false; 244 | } ) 245 | .then( function( values ) { 246 | // format the values 247 | return ask.formatDatavalues( values ); 248 | }, function( state ) { 249 | if ( state !== false ) { 250 | showError( i18n.t( 'nodata', { subject: parsed.article + ' ' + parsed.property + ' ' + parsed.possesive + ' ' + parsed.item } ) ); 251 | } 252 | return false; 253 | } ) 254 | .then( function( formattedValues ) { 255 | // display the values 256 | parsed.value = combineValues( formattedValues ); 257 | console.log( parsed ); 258 | var format = parsed.callback ? parsed.callback : '$article $property $possesive $item $verb $value.'; 259 | var answer = parser.buildAnswer( format, parsed ); 260 | showResult( answer ); 261 | }, function( state ) { 262 | // formatting failed 263 | if ( state !== false ) { 264 | showError( i18n.t( 'notformatted' ) ); 265 | } 266 | return false; 267 | } ); 268 | } 269 | // common question => get the description 270 | else { 271 | api.getEntities( itemId, 'descriptions' ) 272 | .done( function( data ) { 273 | if ( data.entities[itemId].descriptions[api.language()] ) { 274 | showResult( data.entities[itemId].descriptions[api.language()].value ); 275 | } else { 276 | showResult( itemId ); 277 | } 278 | } ); 279 | } 280 | }, function( notfound ) { 281 | // item not found 282 | if ( notfound !== false ) { 283 | showError( i18n.t( 'itemnotfound', { item: parsed.item } ) ); 284 | } else { 285 | showIntro(); 286 | } 287 | } ); 288 | } 289 | 290 | } )( jQuery ); 291 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JavaScript api to parse questions optimized to query data from Wikibase 3 | * Dependencies: 4 | * # jQuery < http://jquery.com/ > 5 | * # XRegExp < http://xregexp.com/ > 6 | * 7 | * @author Bene 8 | * @license GNU GPL v2+ 9 | * @version 0.1 10 | */ 11 | ( function( $, regex, ns ) { 12 | 'use strict'; 13 | 14 | /** 15 | * Contains the regexes used for parsing. 16 | * 17 | * @var {object} 18 | */ 19 | var regexes = {}; 20 | 21 | /** 22 | * Contains shortcut attributes that can be used in the reges. 23 | * 24 | * @var {object} 25 | */ 26 | var attributes = {}; 27 | 28 | /** 29 | * Constructor to create a new parser object. 30 | * 31 | * @param {string} language 32 | * 33 | * @constructor 34 | */ 35 | ns.Parser = function( language ) { 36 | this._language = language; 37 | this._initPatterns(); 38 | }; 39 | 40 | $.extend( ns.Parser.prototype, { 41 | 42 | /** 43 | * Sets or gets the language. 44 | * 45 | * @param {string} language 46 | */ 47 | language: function( language ) { 48 | if ( language ) { 49 | this._language = language; 50 | this._initPatterns(); 51 | } 52 | return this._language; 53 | }, 54 | 55 | /** 56 | * Loads the patterns. 57 | */ 58 | _initPatterns: function() { 59 | this._promise = $.getJSON( 'patterns/' + this._language + '.json', function( data ) { 60 | regexes = data.regexes; 61 | attributes = data.attributes; 62 | } ); 63 | }, 64 | 65 | /** 66 | * Builds an answer string based on the given format string and the given attributes. 67 | * 68 | * @param {string} format 69 | * @param {object} attributes 70 | * @return {string} 71 | */ 72 | buildAnswer: function( format, attributes ) { 73 | for ( var i in attributes ) { 74 | format = format.replace( new RegExp( '\\$' + i, 'g' ), attributes[i] ); 75 | } 76 | //format = format.replace( /undefined /g, '' ); 77 | format = format.replace( /\$[\w\d-_\|]+ ?/g, '' ); 78 | format = format.charAt( 0 ).toUpperCase() + format.slice( 1 ); // ucfirst 79 | return format; 80 | }, 81 | 82 | /** 83 | * Parses the question and returns the an object containing some of the following keys: 84 | * [ 'question', 'verb', 'article', 'property', 'possesive', 'item' ] 85 | * 86 | * @param {string} question 87 | * @return {object} 88 | */ 89 | parseQuestion: function( question ) { 90 | var deferred = $.Deferred(); 91 | this._promise.then( function() { 92 | question = question.trim(); 93 | if ( question.indexOf( '?', question.length - 1 ) !== -1 ) { 94 | question = question.substring( 0, question.length - 1 ); 95 | } 96 | for ( var r in regexes ) { 97 | var regString = regexes[r].regex; 98 | for ( var a in attributes ) { 99 | regString = regString.replace( new RegExp( '\\$' + a, 'g' ), attributes[a] ); 100 | } 101 | var reg = regex( '^' + regString + '$', 'i' ); 102 | if ( reg.test( question ) ) { 103 | var parts = regex.exec( question, reg ); 104 | var result = $.extend( {}, regexes[r], parts ); 105 | deferred.resolve( result ); 106 | return; 107 | } 108 | } 109 | deferred.reject(); 110 | } ); 111 | return deferred.promise(); 112 | } 113 | } ); 114 | 115 | } )( jQuery, XRegExp, window ); -------------------------------------------------------------------------------- /test/qunit-1.13.0.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.13.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2013 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-01-04T17:09Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699a4; 36 | background-color: #0d3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: normal; 41 | 42 | border-radius: 5px 5px 0 0; 43 | -moz-border-radius: 5px 5px 0 0; 44 | -webkit-border-top-right-radius: 5px; 45 | -webkit-border-top-left-radius: 5px; 46 | } 47 | 48 | #qunit-header a { 49 | text-decoration: none; 50 | color: #c2ccd1; 51 | } 52 | 53 | #qunit-header a:hover, 54 | #qunit-header a:focus { 55 | color: #fff; 56 | } 57 | 58 | #qunit-testrunner-toolbar label { 59 | display: inline-block; 60 | padding: 0 .5em 0 .1em; 61 | } 62 | 63 | #qunit-banner { 64 | height: 5px; 65 | } 66 | 67 | #qunit-testrunner-toolbar { 68 | padding: 0.5em 0 0.5em 2em; 69 | color: #5E740B; 70 | background-color: #eee; 71 | overflow: hidden; 72 | } 73 | 74 | #qunit-userAgent { 75 | padding: 0.5em 0 0.5em 2.5em; 76 | background-color: #2b81af; 77 | color: #fff; 78 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 79 | } 80 | 81 | #qunit-modulefilter-container { 82 | float: right; 83 | } 84 | 85 | /** Tests: Pass/Fail */ 86 | 87 | #qunit-tests { 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests li { 92 | padding: 0.4em 0.5em 0.4em 2.5em; 93 | border-bottom: 1px solid #fff; 94 | list-style-position: inside; 95 | } 96 | 97 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 98 | display: none; 99 | } 100 | 101 | #qunit-tests li strong { 102 | cursor: pointer; 103 | } 104 | 105 | #qunit-tests li a { 106 | padding: 0.5em; 107 | color: #c2ccd1; 108 | text-decoration: none; 109 | } 110 | #qunit-tests li a:hover, 111 | #qunit-tests li a:focus { 112 | color: #000; 113 | } 114 | 115 | #qunit-tests li .runtime { 116 | float: right; 117 | font-size: smaller; 118 | } 119 | 120 | .qunit-assert-list { 121 | margin-top: 0.5em; 122 | padding: 0.5em; 123 | 124 | background-color: #fff; 125 | 126 | border-radius: 5px; 127 | -moz-border-radius: 5px; 128 | -webkit-border-radius: 5px; 129 | } 130 | 131 | .qunit-collapsed { 132 | display: none; 133 | } 134 | 135 | #qunit-tests table { 136 | border-collapse: collapse; 137 | margin-top: .2em; 138 | } 139 | 140 | #qunit-tests th { 141 | text-align: right; 142 | vertical-align: top; 143 | padding: 0 .5em 0 0; 144 | } 145 | 146 | #qunit-tests td { 147 | vertical-align: top; 148 | } 149 | 150 | #qunit-tests pre { 151 | margin: 0; 152 | white-space: pre-wrap; 153 | word-wrap: break-word; 154 | } 155 | 156 | #qunit-tests del { 157 | background-color: #e0f2be; 158 | color: #374e0c; 159 | text-decoration: none; 160 | } 161 | 162 | #qunit-tests ins { 163 | background-color: #ffcaca; 164 | color: #500; 165 | text-decoration: none; 166 | } 167 | 168 | /*** Test Counts */ 169 | 170 | #qunit-tests b.counts { color: black; } 171 | #qunit-tests b.passed { color: #5E740B; } 172 | #qunit-tests b.failed { color: #710909; } 173 | 174 | #qunit-tests li li { 175 | padding: 5px; 176 | background-color: #fff; 177 | border-bottom: none; 178 | list-style-position: inside; 179 | } 180 | 181 | /*** Passing Styles */ 182 | 183 | #qunit-tests li li.pass { 184 | color: #3c510c; 185 | background-color: #fff; 186 | border-left: 10px solid #C6E746; 187 | } 188 | 189 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 190 | #qunit-tests .pass .test-name { color: #366097; } 191 | 192 | #qunit-tests .pass .test-actual, 193 | #qunit-tests .pass .test-expected { color: #999999; } 194 | 195 | #qunit-banner.qunit-pass { background-color: #C6E746; } 196 | 197 | /*** Failing Styles */ 198 | 199 | #qunit-tests li li.fail { 200 | color: #710909; 201 | background-color: #fff; 202 | border-left: 10px solid #EE5757; 203 | white-space: pre; 204 | } 205 | 206 | #qunit-tests > li:last-child { 207 | border-radius: 0 0 5px 5px; 208 | -moz-border-radius: 0 0 5px 5px; 209 | -webkit-border-bottom-right-radius: 5px; 210 | -webkit-border-bottom-left-radius: 5px; 211 | } 212 | 213 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 214 | #qunit-tests .fail .test-name, 215 | #qunit-tests .fail .module-name { color: #000000; } 216 | 217 | #qunit-tests .fail .test-actual { color: #EE5757; } 218 | #qunit-tests .fail .test-expected { color: green; } 219 | 220 | #qunit-banner.qunit-fail { background-color: #EE5757; } 221 | 222 | 223 | /** Result */ 224 | 225 | #qunit-testresult { 226 | padding: 0.5em 0.5em 0.5em 2.5em; 227 | 228 | color: #2b81af; 229 | background-color: #D2E0E6; 230 | 231 | border-bottom: 1px solid white; 232 | } 233 | #qunit-testresult .module-name { 234 | font-weight: bold; 235 | } 236 | 237 | /** Fixture */ 238 | 239 | #qunit-fixture { 240 | position: absolute; 241 | top: -10000px; 242 | left: -10000px; 243 | width: 1000px; 244 | height: 1000px; 245 | } 246 | -------------------------------------------------------------------------------- /test/qunit-1.13.0.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 1.13.0 3 | * http://qunitjs.com/ 4 | * 5 | * Copyright 2013 jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * http://jquery.org/license 8 | * 9 | * Date: 2014-01-04T17:09Z 10 | */ 11 | 12 | (function( window ) { 13 | 14 | var QUnit, 15 | assert, 16 | config, 17 | onErrorFnPrev, 18 | testId = 0, 19 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 20 | toString = Object.prototype.toString, 21 | hasOwn = Object.prototype.hasOwnProperty, 22 | // Keep a local reference to Date (GH-283) 23 | Date = window.Date, 24 | setTimeout = window.setTimeout, 25 | defined = { 26 | document: typeof window.document !== "undefined", 27 | setTimeout: typeof window.setTimeout !== "undefined", 28 | sessionStorage: (function() { 29 | var x = "qunit-test-string"; 30 | try { 31 | sessionStorage.setItem( x, x ); 32 | sessionStorage.removeItem( x ); 33 | return true; 34 | } catch( e ) { 35 | return false; 36 | } 37 | }()) 38 | }, 39 | /** 40 | * Provides a normalized error string, correcting an issue 41 | * with IE 7 (and prior) where Error.prototype.toString is 42 | * not properly implemented 43 | * 44 | * Based on http://es5.github.com/#x15.11.4.4 45 | * 46 | * @param {String|Error} error 47 | * @return {String} error message 48 | */ 49 | errorString = function( error ) { 50 | var name, message, 51 | errorString = error.toString(); 52 | if ( errorString.substring( 0, 7 ) === "[object" ) { 53 | name = error.name ? error.name.toString() : "Error"; 54 | message = error.message ? error.message.toString() : ""; 55 | if ( name && message ) { 56 | return name + ": " + message; 57 | } else if ( name ) { 58 | return name; 59 | } else if ( message ) { 60 | return message; 61 | } else { 62 | return "Error"; 63 | } 64 | } else { 65 | return errorString; 66 | } 67 | }, 68 | /** 69 | * Makes a clone of an object using only Array or Object as base, 70 | * and copies over the own enumerable properties. 71 | * 72 | * @param {Object} obj 73 | * @return {Object} New object with only the own properties (recursively). 74 | */ 75 | objectValues = function( obj ) { 76 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 77 | /*jshint newcap: false */ 78 | var key, val, 79 | vals = QUnit.is( "array", obj ) ? [] : {}; 80 | for ( key in obj ) { 81 | if ( hasOwn.call( obj, key ) ) { 82 | val = obj[key]; 83 | vals[key] = val === Object(val) ? objectValues(val) : val; 84 | } 85 | } 86 | return vals; 87 | }; 88 | 89 | 90 | // Root QUnit object. 91 | // `QUnit` initialized at top of scope 92 | QUnit = { 93 | 94 | // call on start of module test to prepend name to all tests 95 | module: function( name, testEnvironment ) { 96 | config.currentModule = name; 97 | config.currentModuleTestEnvironment = testEnvironment; 98 | config.modules[name] = true; 99 | }, 100 | 101 | asyncTest: function( testName, expected, callback ) { 102 | if ( arguments.length === 2 ) { 103 | callback = expected; 104 | expected = null; 105 | } 106 | 107 | QUnit.test( testName, expected, callback, true ); 108 | }, 109 | 110 | test: function( testName, expected, callback, async ) { 111 | var test, 112 | nameHtml = "" + escapeText( testName ) + ""; 113 | 114 | if ( arguments.length === 2 ) { 115 | callback = expected; 116 | expected = null; 117 | } 118 | 119 | if ( config.currentModule ) { 120 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 121 | } 122 | 123 | test = new Test({ 124 | nameHtml: nameHtml, 125 | testName: testName, 126 | expected: expected, 127 | async: async, 128 | callback: callback, 129 | module: config.currentModule, 130 | moduleTestEnvironment: config.currentModuleTestEnvironment, 131 | stack: sourceFromStacktrace( 2 ) 132 | }); 133 | 134 | if ( !validTest( test ) ) { 135 | return; 136 | } 137 | 138 | test.queue(); 139 | }, 140 | 141 | // Specify the number of expected assertions to guarantee that failed test (no assertions are run at all) don't slip through. 142 | expect: function( asserts ) { 143 | if (arguments.length === 1) { 144 | config.current.expected = asserts; 145 | } else { 146 | return config.current.expected; 147 | } 148 | }, 149 | 150 | start: function( count ) { 151 | // QUnit hasn't been initialized yet. 152 | // Note: RequireJS (et al) may delay onLoad 153 | if ( config.semaphore === undefined ) { 154 | QUnit.begin(function() { 155 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 156 | setTimeout(function() { 157 | QUnit.start( count ); 158 | }); 159 | }); 160 | return; 161 | } 162 | 163 | config.semaphore -= count || 1; 164 | // don't start until equal number of stop-calls 165 | if ( config.semaphore > 0 ) { 166 | return; 167 | } 168 | // ignore if start is called more often then stop 169 | if ( config.semaphore < 0 ) { 170 | config.semaphore = 0; 171 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 172 | return; 173 | } 174 | // A slight delay, to avoid any current callbacks 175 | if ( defined.setTimeout ) { 176 | setTimeout(function() { 177 | if ( config.semaphore > 0 ) { 178 | return; 179 | } 180 | if ( config.timeout ) { 181 | clearTimeout( config.timeout ); 182 | } 183 | 184 | config.blocking = false; 185 | process( true ); 186 | }, 13); 187 | } else { 188 | config.blocking = false; 189 | process( true ); 190 | } 191 | }, 192 | 193 | stop: function( count ) { 194 | config.semaphore += count || 1; 195 | config.blocking = true; 196 | 197 | if ( config.testTimeout && defined.setTimeout ) { 198 | clearTimeout( config.timeout ); 199 | config.timeout = setTimeout(function() { 200 | QUnit.ok( false, "Test timed out" ); 201 | config.semaphore = 1; 202 | QUnit.start(); 203 | }, config.testTimeout ); 204 | } 205 | } 206 | }; 207 | 208 | // We use the prototype to distinguish between properties that should 209 | // be exposed as globals (and in exports) and those that shouldn't 210 | (function() { 211 | function F() {} 212 | F.prototype = QUnit; 213 | QUnit = new F(); 214 | // Make F QUnit's constructor so that we can add to the prototype later 215 | QUnit.constructor = F; 216 | }()); 217 | 218 | /** 219 | * Config object: Maintain internal state 220 | * Later exposed as QUnit.config 221 | * `config` initialized at top of scope 222 | */ 223 | config = { 224 | // The queue of tests to run 225 | queue: [], 226 | 227 | // block until document ready 228 | blocking: true, 229 | 230 | // when enabled, show only failing tests 231 | // gets persisted through sessionStorage and can be changed in UI via checkbox 232 | hidepassed: false, 233 | 234 | // by default, run previously failed tests first 235 | // very useful in combination with "Hide passed tests" checked 236 | reorder: true, 237 | 238 | // by default, modify document.title when suite is done 239 | altertitle: true, 240 | 241 | // when enabled, all tests must call expect() 242 | requireExpects: false, 243 | 244 | // add checkboxes that are persisted in the query-string 245 | // when enabled, the id is set to `true` as a `QUnit.config` property 246 | urlConfig: [ 247 | { 248 | id: "noglobals", 249 | label: "Check for Globals", 250 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 251 | }, 252 | { 253 | id: "notrycatch", 254 | label: "No try-catch", 255 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 256 | } 257 | ], 258 | 259 | // Set of all modules. 260 | modules: {}, 261 | 262 | // logging callback queues 263 | begin: [], 264 | done: [], 265 | log: [], 266 | testStart: [], 267 | testDone: [], 268 | moduleStart: [], 269 | moduleDone: [] 270 | }; 271 | 272 | // Initialize more QUnit.config and QUnit.urlParams 273 | (function() { 274 | var i, 275 | location = window.location || { search: "", protocol: "file:" }, 276 | params = location.search.slice( 1 ).split( "&" ), 277 | length = params.length, 278 | urlParams = {}, 279 | current; 280 | 281 | if ( params[ 0 ] ) { 282 | for ( i = 0; i < length; i++ ) { 283 | current = params[ i ].split( "=" ); 284 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 285 | // allow just a key to turn on a flag, e.g., test.html?noglobals 286 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 287 | urlParams[ current[ 0 ] ] = current[ 1 ]; 288 | } 289 | } 290 | 291 | QUnit.urlParams = urlParams; 292 | 293 | // String search anywhere in moduleName+testName 294 | config.filter = urlParams.filter; 295 | 296 | // Exact match of the module name 297 | config.module = urlParams.module; 298 | 299 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 300 | 301 | // Figure out if we're running the tests from a server or not 302 | QUnit.isLocal = location.protocol === "file:"; 303 | }()); 304 | 305 | extend( QUnit, { 306 | 307 | config: config, 308 | 309 | // Initialize the configuration options 310 | init: function() { 311 | extend( config, { 312 | stats: { all: 0, bad: 0 }, 313 | moduleStats: { all: 0, bad: 0 }, 314 | started: +new Date(), 315 | updateRate: 1000, 316 | blocking: false, 317 | autostart: true, 318 | autorun: false, 319 | filter: "", 320 | queue: [], 321 | semaphore: 1 322 | }); 323 | 324 | var tests, banner, result, 325 | qunit = id( "qunit" ); 326 | 327 | if ( qunit ) { 328 | qunit.innerHTML = 329 | "
" + escapeText( document.title ) + "
" + 330 | "" + 331 | "" + 332 | "" + 333 | ""; 358 | } 359 | }, 360 | 361 | // Resets the test setup. Useful for tests that modify the DOM. 362 | /* 363 | DEPRECATED: Use multiple tests instead of resetting inside a test. 364 | Use testStart or testDone for custom cleanup. 365 | This method will throw an error in 2.0, and will be removed in 2.1 366 | */ 367 | reset: function() { 368 | var fixture = id( "qunit-fixture" ); 369 | if ( fixture ) { 370 | fixture.innerHTML = config.fixture; 371 | } 372 | }, 373 | 374 | // Safe object type checking 375 | is: function( type, obj ) { 376 | return QUnit.objectType( obj ) === type; 377 | }, 378 | 379 | objectType: function( obj ) { 380 | if ( typeof obj === "undefined" ) { 381 | return "undefined"; 382 | } 383 | 384 | // Consider: typeof null === object 385 | if ( obj === null ) { 386 | return "null"; 387 | } 388 | 389 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 390 | type = match && match[1] || ""; 391 | 392 | switch ( type ) { 393 | case "Number": 394 | if ( isNaN(obj) ) { 395 | return "nan"; 396 | } 397 | return "number"; 398 | case "String": 399 | case "Boolean": 400 | case "Array": 401 | case "Date": 402 | case "RegExp": 403 | case "Function": 404 | return type.toLowerCase(); 405 | } 406 | if ( typeof obj === "object" ) { 407 | return "object"; 408 | } 409 | return undefined; 410 | }, 411 | 412 | push: function( result, actual, expected, message ) { 413 | if ( !config.current ) { 414 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 415 | } 416 | 417 | var output, source, 418 | details = { 419 | module: config.current.module, 420 | name: config.current.testName, 421 | result: result, 422 | message: message, 423 | actual: actual, 424 | expected: expected 425 | }; 426 | 427 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 428 | message = " "; 429 | output = message; 430 | 431 | if ( !result ) { 432 | expected = escapeText( QUnit.jsDump.parse(expected) ); 433 | actual = escapeText( QUnit.jsDump.parse(actual) ); 434 | output += "
Expected: | " + expected + " |
---|---|
Result: | " + actual + " |
Diff: | " + QUnit.diff( expected, actual ) + " |
Source: | " + escapeText( source ) + " |
Result: | " + escapeText( actual ) + " |
---|---|
Source: | " + escapeText( source ) + " |
", 772 | "", 773 | passed, 774 | " assertions of ", 775 | config.stats.all, 776 | " passed, ", 777 | config.stats.bad, 778 | " failed." 779 | ].join( "" ); 780 | 781 | if ( banner ) { 782 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 783 | } 784 | 785 | if ( tests ) { 786 | id( "qunit-testresult" ).innerHTML = html; 787 | } 788 | 789 | if ( config.altertitle && defined.document && document.title ) { 790 | // show ✖ for good, ✔ for bad suite result in title 791 | // use escape sequences in case file gets loaded with non-utf-8-charset 792 | document.title = [ 793 | ( config.stats.bad ? "\u2716" : "\u2714" ), 794 | document.title.replace( /^[\u2714\u2716] /i, "" ) 795 | ].join( " " ); 796 | } 797 | 798 | // clear own sessionStorage items if all tests passed 799 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 800 | // `key` & `i` initialized at top of scope 801 | for ( i = 0; i < sessionStorage.length; i++ ) { 802 | key = sessionStorage.key( i++ ); 803 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 804 | sessionStorage.removeItem( key ); 805 | } 806 | } 807 | } 808 | 809 | // scroll back to top to show results 810 | if ( window.scrollTo ) { 811 | window.scrollTo(0, 0); 812 | } 813 | 814 | runLoggingCallbacks( "done", QUnit, { 815 | failed: config.stats.bad, 816 | passed: passed, 817 | total: config.stats.all, 818 | runtime: runtime 819 | }); 820 | } 821 | 822 | /** @return Boolean: true if this test should be ran */ 823 | function validTest( test ) { 824 | var include, 825 | filter = config.filter && config.filter.toLowerCase(), 826 | module = config.module && config.module.toLowerCase(), 827 | fullName = (test.module + ": " + test.testName).toLowerCase(); 828 | 829 | // Internally-generated tests are always valid 830 | if ( test.callback && test.callback.validTest === validTest ) { 831 | delete test.callback.validTest; 832 | return true; 833 | } 834 | 835 | if ( config.testNumber ) { 836 | return test.testNumber === config.testNumber; 837 | } 838 | 839 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 840 | return false; 841 | } 842 | 843 | if ( !filter ) { 844 | return true; 845 | } 846 | 847 | include = filter.charAt( 0 ) !== "!"; 848 | if ( !include ) { 849 | filter = filter.slice( 1 ); 850 | } 851 | 852 | // If the filter matches, we need to honour include 853 | if ( fullName.indexOf( filter ) !== -1 ) { 854 | return include; 855 | } 856 | 857 | // Otherwise, do the opposite 858 | return !include; 859 | } 860 | 861 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 862 | // Later Safari and IE10 are supposed to support error.stack as well 863 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 864 | function extractStacktrace( e, offset ) { 865 | offset = offset === undefined ? 3 : offset; 866 | 867 | var stack, include, i; 868 | 869 | if ( e.stacktrace ) { 870 | // Opera 871 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 872 | } else if ( e.stack ) { 873 | // Firefox, Chrome 874 | stack = e.stack.split( "\n" ); 875 | if (/^error$/i.test( stack[0] ) ) { 876 | stack.shift(); 877 | } 878 | if ( fileName ) { 879 | include = []; 880 | for ( i = offset; i < stack.length; i++ ) { 881 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 882 | break; 883 | } 884 | include.push( stack[ i ] ); 885 | } 886 | if ( include.length ) { 887 | return include.join( "\n" ); 888 | } 889 | } 890 | return stack[ offset ]; 891 | } else if ( e.sourceURL ) { 892 | // Safari, PhantomJS 893 | // hopefully one day Safari provides actual stacktraces 894 | // exclude useless self-reference for generated Error objects 895 | if ( /qunit.js$/.test( e.sourceURL ) ) { 896 | return; 897 | } 898 | // for actual exceptions, this is useful 899 | return e.sourceURL + ":" + e.line; 900 | } 901 | } 902 | function sourceFromStacktrace( offset ) { 903 | try { 904 | throw new Error(); 905 | } catch ( e ) { 906 | return extractStacktrace( e, offset ); 907 | } 908 | } 909 | 910 | /** 911 | * Escape text for attribute or text content. 912 | */ 913 | function escapeText( s ) { 914 | if ( !s ) { 915 | return ""; 916 | } 917 | s = s + ""; 918 | // Both single quotes and double quotes (for attributes) 919 | return s.replace( /['"<>&]/g, function( s ) { 920 | switch( s ) { 921 | case "'": 922 | return "'"; 923 | case "\"": 924 | return """; 925 | case "<": 926 | return "<"; 927 | case ">": 928 | return ">"; 929 | case "&": 930 | return "&"; 931 | } 932 | }); 933 | } 934 | 935 | function synchronize( callback, last ) { 936 | config.queue.push( callback ); 937 | 938 | if ( config.autorun && !config.blocking ) { 939 | process( last ); 940 | } 941 | } 942 | 943 | function process( last ) { 944 | function next() { 945 | process( last ); 946 | } 947 | var start = new Date().getTime(); 948 | config.depth = config.depth ? config.depth + 1 : 1; 949 | 950 | while ( config.queue.length && !config.blocking ) { 951 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 952 | config.queue.shift()(); 953 | } else { 954 | setTimeout( next, 13 ); 955 | break; 956 | } 957 | } 958 | config.depth--; 959 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 960 | done(); 961 | } 962 | } 963 | 964 | function saveGlobal() { 965 | config.pollution = []; 966 | 967 | if ( config.noglobals ) { 968 | for ( var key in window ) { 969 | if ( hasOwn.call( window, key ) ) { 970 | // in Opera sometimes DOM element ids show up here, ignore them 971 | if ( /^qunit-test-output/.test( key ) ) { 972 | continue; 973 | } 974 | config.pollution.push( key ); 975 | } 976 | } 977 | } 978 | } 979 | 980 | function checkPollution() { 981 | var newGlobals, 982 | deletedGlobals, 983 | old = config.pollution; 984 | 985 | saveGlobal(); 986 | 987 | newGlobals = diff( config.pollution, old ); 988 | if ( newGlobals.length > 0 ) { 989 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 990 | } 991 | 992 | deletedGlobals = diff( old, config.pollution ); 993 | if ( deletedGlobals.length > 0 ) { 994 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 995 | } 996 | } 997 | 998 | // returns a new Array with the elements that are in a but not in b 999 | function diff( a, b ) { 1000 | var i, j, 1001 | result = a.slice(); 1002 | 1003 | for ( i = 0; i < result.length; i++ ) { 1004 | for ( j = 0; j < b.length; j++ ) { 1005 | if ( result[i] === b[j] ) { 1006 | result.splice( i, 1 ); 1007 | i--; 1008 | break; 1009 | } 1010 | } 1011 | } 1012 | return result; 1013 | } 1014 | 1015 | function extend( a, b ) { 1016 | for ( var prop in b ) { 1017 | if ( hasOwn.call( b, prop ) ) { 1018 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor 1019 | if ( !( prop === "constructor" && a === window ) ) { 1020 | if ( b[ prop ] === undefined ) { 1021 | delete a[ prop ]; 1022 | } else { 1023 | a[ prop ] = b[ prop ]; 1024 | } 1025 | } 1026 | } 1027 | } 1028 | 1029 | return a; 1030 | } 1031 | 1032 | /** 1033 | * @param {HTMLElement} elem 1034 | * @param {string} type 1035 | * @param {Function} fn 1036 | */ 1037 | function addEvent( elem, type, fn ) { 1038 | if ( elem.addEventListener ) { 1039 | 1040 | // Standards-based browsers 1041 | elem.addEventListener( type, fn, false ); 1042 | } else if ( elem.attachEvent ) { 1043 | 1044 | // support: IE <9 1045 | elem.attachEvent( "on" + type, fn ); 1046 | } else { 1047 | 1048 | // Caller must ensure support for event listeners is present 1049 | throw new Error( "addEvent() was called in a context without event listener support" ); 1050 | } 1051 | } 1052 | 1053 | /** 1054 | * @param {Array|NodeList} elems 1055 | * @param {string} type 1056 | * @param {Function} fn 1057 | */ 1058 | function addEvents( elems, type, fn ) { 1059 | var i = elems.length; 1060 | while ( i-- ) { 1061 | addEvent( elems[i], type, fn ); 1062 | } 1063 | } 1064 | 1065 | function hasClass( elem, name ) { 1066 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1067 | } 1068 | 1069 | function addClass( elem, name ) { 1070 | if ( !hasClass( elem, name ) ) { 1071 | elem.className += (elem.className ? " " : "") + name; 1072 | } 1073 | } 1074 | 1075 | function removeClass( elem, name ) { 1076 | var set = " " + elem.className + " "; 1077 | // Class name may appear multiple times 1078 | while ( set.indexOf(" " + name + " ") > -1 ) { 1079 | set = set.replace(" " + name + " " , " "); 1080 | } 1081 | // If possible, trim it for prettiness, but not necessarily 1082 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); 1083 | } 1084 | 1085 | function id( name ) { 1086 | return defined.document && document.getElementById && document.getElementById( name ); 1087 | } 1088 | 1089 | function registerLoggingCallback( key ) { 1090 | return function( callback ) { 1091 | config[key].push( callback ); 1092 | }; 1093 | } 1094 | 1095 | // Supports deprecated method of completely overwriting logging callbacks 1096 | function runLoggingCallbacks( key, scope, args ) { 1097 | var i, callbacks; 1098 | if ( QUnit.hasOwnProperty( key ) ) { 1099 | QUnit[ key ].call(scope, args ); 1100 | } else { 1101 | callbacks = config[ key ]; 1102 | for ( i = 0; i < callbacks.length; i++ ) { 1103 | callbacks[ i ].call( scope, args ); 1104 | } 1105 | } 1106 | } 1107 | 1108 | // from jquery.js 1109 | function inArray( elem, array ) { 1110 | if ( array.indexOf ) { 1111 | return array.indexOf( elem ); 1112 | } 1113 | 1114 | for ( var i = 0, length = array.length; i < length; i++ ) { 1115 | if ( array[ i ] === elem ) { 1116 | return i; 1117 | } 1118 | } 1119 | 1120 | return -1; 1121 | } 1122 | 1123 | function Test( settings ) { 1124 | extend( this, settings ); 1125 | this.assertions = []; 1126 | this.testNumber = ++Test.count; 1127 | } 1128 | 1129 | Test.count = 0; 1130 | 1131 | Test.prototype = { 1132 | init: function() { 1133 | var a, b, li, 1134 | tests = id( "qunit-tests" ); 1135 | 1136 | if ( tests ) { 1137 | b = document.createElement( "strong" ); 1138 | b.innerHTML = this.nameHtml; 1139 | 1140 | // `a` initialized at top of scope 1141 | a = document.createElement( "a" ); 1142 | a.innerHTML = "Rerun"; 1143 | a.href = QUnit.url({ testNumber: this.testNumber }); 1144 | 1145 | li = document.createElement( "li" ); 1146 | li.appendChild( b ); 1147 | li.appendChild( a ); 1148 | li.className = "running"; 1149 | li.id = this.id = "qunit-test-output" + testId++; 1150 | 1151 | tests.appendChild( li ); 1152 | } 1153 | }, 1154 | setup: function() { 1155 | if ( 1156 | // Emit moduleStart when we're switching from one module to another 1157 | this.module !== config.previousModule || 1158 | // They could be equal (both undefined) but if the previousModule property doesn't 1159 | // yet exist it means this is the first test in a suite that isn't wrapped in a 1160 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 1161 | // Without this, reporters can get testStart before moduleStart which is a problem. 1162 | !hasOwn.call( config, "previousModule" ) 1163 | ) { 1164 | if ( hasOwn.call( config, "previousModule" ) ) { 1165 | runLoggingCallbacks( "moduleDone", QUnit, { 1166 | name: config.previousModule, 1167 | failed: config.moduleStats.bad, 1168 | passed: config.moduleStats.all - config.moduleStats.bad, 1169 | total: config.moduleStats.all 1170 | }); 1171 | } 1172 | config.previousModule = this.module; 1173 | config.moduleStats = { all: 0, bad: 0 }; 1174 | runLoggingCallbacks( "moduleStart", QUnit, { 1175 | name: this.module 1176 | }); 1177 | } 1178 | 1179 | config.current = this; 1180 | 1181 | this.testEnvironment = extend({ 1182 | setup: function() {}, 1183 | teardown: function() {} 1184 | }, this.moduleTestEnvironment ); 1185 | 1186 | this.started = +new Date(); 1187 | runLoggingCallbacks( "testStart", QUnit, { 1188 | name: this.testName, 1189 | module: this.module 1190 | }); 1191 | 1192 | /*jshint camelcase:false */ 1193 | 1194 | 1195 | /** 1196 | * Expose the current test environment. 1197 | * 1198 | * @deprecated since 1.12.0: Use QUnit.config.current.testEnvironment instead. 1199 | */ 1200 | QUnit.current_testEnvironment = this.testEnvironment; 1201 | 1202 | /*jshint camelcase:true */ 1203 | 1204 | if ( !config.pollution ) { 1205 | saveGlobal(); 1206 | } 1207 | if ( config.notrycatch ) { 1208 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 1209 | return; 1210 | } 1211 | try { 1212 | this.testEnvironment.setup.call( this.testEnvironment, QUnit.assert ); 1213 | } catch( e ) { 1214 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 1215 | } 1216 | }, 1217 | run: function() { 1218 | config.current = this; 1219 | 1220 | var running = id( "qunit-testresult" ); 1221 | 1222 | if ( running ) { 1223 | running.innerHTML = "Running:
" + this.nameHtml; 1224 | } 1225 | 1226 | if ( this.async ) { 1227 | QUnit.stop(); 1228 | } 1229 | 1230 | this.callbackStarted = +new Date(); 1231 | 1232 | if ( config.notrycatch ) { 1233 | this.callback.call( this.testEnvironment, QUnit.assert ); 1234 | this.callbackRuntime = +new Date() - this.callbackStarted; 1235 | return; 1236 | } 1237 | 1238 | try { 1239 | this.callback.call( this.testEnvironment, QUnit.assert ); 1240 | this.callbackRuntime = +new Date() - this.callbackStarted; 1241 | } catch( e ) { 1242 | this.callbackRuntime = +new Date() - this.callbackStarted; 1243 | 1244 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 1245 | // else next test will carry the responsibility 1246 | saveGlobal(); 1247 | 1248 | // Restart the tests if they're blocking 1249 | if ( config.blocking ) { 1250 | QUnit.start(); 1251 | } 1252 | } 1253 | }, 1254 | teardown: function() { 1255 | config.current = this; 1256 | if ( config.notrycatch ) { 1257 | if ( typeof this.callbackRuntime === "undefined" ) { 1258 | this.callbackRuntime = +new Date() - this.callbackStarted; 1259 | } 1260 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 1261 | return; 1262 | } else { 1263 | try { 1264 | this.testEnvironment.teardown.call( this.testEnvironment, QUnit.assert ); 1265 | } catch( e ) { 1266 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 1267 | } 1268 | } 1269 | checkPollution(); 1270 | }, 1271 | finish: function() { 1272 | config.current = this; 1273 | if ( config.requireExpects && this.expected === null ) { 1274 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 1275 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 1276 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 1277 | } else if ( this.expected === null && !this.assertions.length ) { 1278 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 1279 | } 1280 | 1281 | var i, assertion, a, b, time, li, ol, 1282 | test = this, 1283 | good = 0, 1284 | bad = 0, 1285 | tests = id( "qunit-tests" ); 1286 | 1287 | this.runtime = +new Date() - this.started; 1288 | config.stats.all += this.assertions.length; 1289 | config.moduleStats.all += this.assertions.length; 1290 | 1291 | if ( tests ) { 1292 | ol = document.createElement( "ol" ); 1293 | ol.className = "qunit-assert-list"; 1294 | 1295 | for ( i = 0; i < this.assertions.length; i++ ) { 1296 | assertion = this.assertions[i]; 1297 | 1298 | li = document.createElement( "li" ); 1299 | li.className = assertion.result ? "pass" : "fail"; 1300 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 1301 | ol.appendChild( li ); 1302 | 1303 | if ( assertion.result ) { 1304 | good++; 1305 | } else { 1306 | bad++; 1307 | config.stats.bad++; 1308 | config.moduleStats.bad++; 1309 | } 1310 | } 1311 | 1312 | // store result when possible 1313 | if ( QUnit.config.reorder && defined.sessionStorage ) { 1314 | if ( bad ) { 1315 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 1316 | } else { 1317 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 1318 | } 1319 | } 1320 | 1321 | if ( bad === 0 ) { 1322 | addClass( ol, "qunit-collapsed" ); 1323 | } 1324 | 1325 | // `b` initialized at top of scope 1326 | b = document.createElement( "strong" ); 1327 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 1328 | 1329 | addEvent(b, "click", function() { 1330 | var next = b.parentNode.lastChild, 1331 | collapsed = hasClass( next, "qunit-collapsed" ); 1332 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 1333 | }); 1334 | 1335 | addEvent(b, "dblclick", function( e ) { 1336 | var target = e && e.target ? e.target : window.event.srcElement; 1337 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 1338 | target = target.parentNode; 1339 | } 1340 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 1341 | window.location = QUnit.url({ testNumber: test.testNumber }); 1342 | } 1343 | }); 1344 | 1345 | // `time` initialized at top of scope 1346 | time = document.createElement( "span" ); 1347 | time.className = "runtime"; 1348 | time.innerHTML = this.runtime + " ms"; 1349 | 1350 | // `li` initialized at top of scope 1351 | li = id( this.id ); 1352 | li.className = bad ? "fail" : "pass"; 1353 | li.removeChild( li.firstChild ); 1354 | a = li.firstChild; 1355 | li.appendChild( b ); 1356 | li.appendChild( a ); 1357 | li.appendChild( time ); 1358 | li.appendChild( ol ); 1359 | 1360 | } else { 1361 | for ( i = 0; i < this.assertions.length; i++ ) { 1362 | if ( !this.assertions[i].result ) { 1363 | bad++; 1364 | config.stats.bad++; 1365 | config.moduleStats.bad++; 1366 | } 1367 | } 1368 | } 1369 | 1370 | runLoggingCallbacks( "testDone", QUnit, { 1371 | name: this.testName, 1372 | module: this.module, 1373 | failed: bad, 1374 | passed: this.assertions.length - bad, 1375 | total: this.assertions.length, 1376 | runtime: this.runtime, 1377 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead 1378 | duration: this.runtime, 1379 | }); 1380 | 1381 | QUnit.reset(); 1382 | 1383 | config.current = undefined; 1384 | }, 1385 | 1386 | queue: function() { 1387 | var bad, 1388 | test = this; 1389 | 1390 | synchronize(function() { 1391 | test.init(); 1392 | }); 1393 | function run() { 1394 | // each of these can by async 1395 | synchronize(function() { 1396 | test.setup(); 1397 | }); 1398 | synchronize(function() { 1399 | test.run(); 1400 | }); 1401 | synchronize(function() { 1402 | test.teardown(); 1403 | }); 1404 | synchronize(function() { 1405 | test.finish(); 1406 | }); 1407 | } 1408 | 1409 | // `bad` initialized at top of scope 1410 | // defer when previous test run passed, if storage is available 1411 | bad = QUnit.config.reorder && defined.sessionStorage && 1412 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 1413 | 1414 | if ( bad ) { 1415 | run(); 1416 | } else { 1417 | synchronize( run, true ); 1418 | } 1419 | } 1420 | }; 1421 | 1422 | // `assert` initialized at top of scope 1423 | // Assert helpers 1424 | // All of these must either call QUnit.push() or manually do: 1425 | // - runLoggingCallbacks( "log", .. ); 1426 | // - config.current.assertions.push({ .. }); 1427 | assert = QUnit.assert = { 1428 | /** 1429 | * Asserts rough true-ish result. 1430 | * @name ok 1431 | * @function 1432 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 1433 | */ 1434 | ok: function( result, msg ) { 1435 | if ( !config.current ) { 1436 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 1437 | } 1438 | result = !!result; 1439 | msg = msg || ( result ? "okay" : "failed" ); 1440 | 1441 | var source, 1442 | details = { 1443 | module: config.current.module, 1444 | name: config.current.testName, 1445 | result: result, 1446 | message: msg 1447 | }; 1448 | 1449 | msg = " "; 1450 | 1451 | if ( !result ) { 1452 | source = sourceFromStacktrace( 2 ); 1453 | if ( source ) { 1454 | details.source = source; 1455 | msg += "
Source: | " + 1456 | escapeText( source ) + 1457 | " |
---|
" : "\n" : this.HTML ? " " : " "; 1911 | }, 1912 | // extra can be a number, shortcut for increasing-calling-decreasing 1913 | indent: function( extra ) { 1914 | if ( !this.multiline ) { 1915 | return ""; 1916 | } 1917 | var chr = this.indentChar; 1918 | if ( this.HTML ) { 1919 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1920 | } 1921 | return new Array( this.depth + ( extra || 0 ) ).join(chr); 1922 | }, 1923 | up: function( a ) { 1924 | this.depth += a || 1; 1925 | }, 1926 | down: function( a ) { 1927 | this.depth -= a || 1; 1928 | }, 1929 | setParser: function( name, parser ) { 1930 | this.parsers[name] = parser; 1931 | }, 1932 | // The next 3 are exposed so you can use them 1933 | quote: quote, 1934 | literal: literal, 1935 | join: join, 1936 | // 1937 | depth: 1, 1938 | // This is the list of parsers, to modify them, use jsDump.setParser 1939 | parsers: { 1940 | window: "[Window]", 1941 | document: "[Document]", 1942 | error: function(error) { 1943 | return "Error(\"" + error.message + "\")"; 1944 | }, 1945 | unknown: "[Unknown]", 1946 | "null": "null", 1947 | "undefined": "undefined", 1948 | "function": function( fn ) { 1949 | var ret = "function", 1950 | // functions never have name in IE 1951 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1952 | 1953 | if ( name ) { 1954 | ret += " " + name; 1955 | } 1956 | ret += "( "; 1957 | 1958 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1959 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1960 | }, 1961 | array: array, 1962 | nodelist: array, 1963 | "arguments": array, 1964 | object: function( map, stack ) { 1965 | /*jshint forin:false */ 1966 | var ret = [ ], keys, key, val, i; 1967 | QUnit.jsDump.up(); 1968 | keys = []; 1969 | for ( key in map ) { 1970 | keys.push( key ); 1971 | } 1972 | keys.sort(); 1973 | for ( i = 0; i < keys.length; i++ ) { 1974 | key = keys[ i ]; 1975 | val = map[ key ]; 1976 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1977 | } 1978 | QUnit.jsDump.down(); 1979 | return join( "{", ret, "}" ); 1980 | }, 1981 | node: function( node ) { 1982 | var len, i, val, 1983 | open = QUnit.jsDump.HTML ? "<" : "<", 1984 | close = QUnit.jsDump.HTML ? ">" : ">", 1985 | tag = node.nodeName.toLowerCase(), 1986 | ret = open + tag, 1987 | attrs = node.attributes; 1988 | 1989 | if ( attrs ) { 1990 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1991 | val = attrs[i].nodeValue; 1992 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1993 | // Those have values like undefined, null, 0, false, "" or "inherit". 1994 | if ( val && val !== "inherit" ) { 1995 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1996 | } 1997 | } 1998 | } 1999 | ret += close; 2000 | 2001 | // Show content of TextNode or CDATASection 2002 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 2003 | ret += node.nodeValue; 2004 | } 2005 | 2006 | return ret + open + "/" + tag + close; 2007 | }, 2008 | // function calls it internally, it's the arguments part of the function 2009 | functionArgs: function( fn ) { 2010 | var args, 2011 | l = fn.length; 2012 | 2013 | if ( !l ) { 2014 | return ""; 2015 | } 2016 | 2017 | args = new Array(l); 2018 | while ( l-- ) { 2019 | // 97 is 'a' 2020 | args[l] = String.fromCharCode(97+l); 2021 | } 2022 | return " " + args.join( ", " ) + " "; 2023 | }, 2024 | // object calls it internally, the key part of an item in a map 2025 | key: quote, 2026 | // function calls it internally, it's the content of the function 2027 | functionCode: "[code]", 2028 | // node calls it internally, it's an html attribute value 2029 | attribute: quote, 2030 | string: quote, 2031 | date: quote, 2032 | regexp: literal, 2033 | number: literal, 2034 | "boolean": literal 2035 | }, 2036 | // if true, entities are escaped ( <, >, \t, space and \n ) 2037 | HTML: false, 2038 | // indentation unit 2039 | indentChar: " ", 2040 | // if true, items in a collection, are separated by a \n, else just a space. 2041 | multiline: true 2042 | }; 2043 | 2044 | return jsDump; 2045 | }()); 2046 | 2047 | /* 2048 | * Javascript Diff Algorithm 2049 | * By John Resig (http://ejohn.org/) 2050 | * Modified by Chu Alan "sprite" 2051 | * 2052 | * Released under the MIT license. 2053 | * 2054 | * More Info: 2055 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2056 | * 2057 | * Usage: QUnit.diff(expected, actual) 2058 | * 2059 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick