jsPerf.app is an online JavaScript performance benchmark test runner & jsperf.com mirror. It is a complete rewrite in homage to the once excellent jsperf.com now with hopefully a more modern & maintainable codebase.
jsperf.com URLs are mirrored at the same path, e.g:
https://jsperf.com/negative-modulo/2
Can be accessed at:
https://jsperf.app/negative-modulo/2
if (!Array.prototype.find) {
Array.prototype.find = function (callback, thisArg) {
'use strict';
var arr = this,
arrLen = arr.length,
i;
for (i = 0; i < arrLen; i += 1) {
if (callback.call(thisArg, arr[i], i, arr)) {
return arr[i];
}
}
return undefined;
};
}
var gms = {};
gms.Observer = function( object, property, callback, context, options ) {
if( typeof object !== 'object' ) {
return;
}
if( ! ( this instanceof gms.Observer )) {
return new gms.Observer( object, property, callback, context, options );
}
this.object = object;
this.property = property;
this.callback = callback;
this.context = context;
this.options = options || {};
this.sync = ( options ) ? options.sync : false;
// Store the observe function to call ( sync or async )
this.observeFunction = ( this.sync ) ? gms.Observe.propertySync : gms.Observe.property;
if( this.sync ) {
this.useNative = false;
} else {
this.useNative = ( options && options.useNative !== undefined ) ? options.useNative : Object.observe !== undefined;
}
// this.useNative = false;
// If we've already created the observer, let's use that
var observer = this._observerAlreadyAttached();
if( observer && observer.length > 0 ){
return observer;
}
if( property.indexOf( '.' ) > -1 ) {
return this._observeChildren( );
}
this._observingArray = (
object[ property ] instanceof Array ||
object instanceof Array && property.indexOf( '@each' ) === 0
);
this._attachObservers();
return this._attachCallbackFunction();
};
gms.Observer.prototype = {
/**
* Attaches the appropriate observes ( Object.observe or getter/setter )
* @return {null}
*/
_attachObservers: function() {
if( this._observingArray ){
// If the property starts with @each, and our object is an array, use that
if( ! ( this.property.indexOf( '@each' === 0 ) && this.object instanceof Array ) ) {
this.object = this.object[ this.property ];
}
if( this.useNative ) {
this._addArrayObjectObserver( this.object );
}
else {
this._addArrayMethods( this.object );
this._addSetters( );
}
} else {
if( this.useNative ) {
this._addObjectObserver();
}
else {
this._addSetters( );
}
}
},
/**
* Handles observing arrays
* @param {Object} object - The object to observe
* @param {String} property - The array property
* @param {String} eachProp - The property to observe on the array
* @return {null}
*/
_observeArray: function( object, property, eachProp ) {
var array = ( property === '@each' ) ? object : object[ property ],
observers = [];
var observer = this.observeFunction.call( gms.Observe, object, property, this.callback, this.context, this.options );
observers.push( observer );
// Add array each observers
if( eachProp ) {
this._addEachProperties( array, eachProp );
array.__eachObservers__[ eachProp ].push( this._createObserver() );
var eachObservers = [];
array.forEach( function( item ) {
eachObservers.push(
this.observeFunction.call( gms.Observe, item, eachProp, this.callback, this.context, this.options )
);
}, this );
observers = observers.concat( eachObservers );
}
return observers;
},
/**
* Adds a property to observe on each item of the array
* @param {Array} array - The array to observe
* @param {String} eachProp - The property to observe on each array item
*/
_addEachProperties: function( array, eachProp ) {
if( ! array.__eachObservers__ ) {
Object.defineProperty( array, '__eachObservers__', {
enumerable : true,
writable : true,
value: {}
} );
}
if( ! array.__eachObservers__[ eachProp ] ) {
Object.defineProperty( array.__eachObservers__, eachProp, {
enumerable : true,
writable : true,
value : []
} );
}
},
/**
* Attaches the callback function to the object.property
* @return { Observer} - The Observer created
*/
_attachCallbackFunction: function() {
// If we're observing an array, we want the __observes__ to be an array,
// since there's no property name for an object
var __observersValue__ = ( this._observingArray ) ? [] : {};
// Create the __observers__ property
if( this.object.__observers__ === undefined ) {
Object.defineProperty( this.object, '__observers__', {
enumerable : false,
value : __observersValue__,
writable : true
} );
}
// Create the __observers__[ property ] array to store the observers
if( !this._observingArray && this.object.__observers__[ this.property ] === undefined ) {
var observers = this.object.__observers__;
Object.defineProperty( observers, this.property, {
enumerable : true,
value : [],
writable : true
} );
}
// Create the Observer actual observer
var observer = this._createObserver();
// Add it to the object.__observers__ array
if( !this._observingArray ){
this.object.__observers__[ this.property ].push ( observer );
} else {
this.object.__observers__.push ( observer );
}
return observer;
},
/**
* Observe child properties in dot notation ( object, 'prop.subProp' )
* @optional {Object} object - The object to observe
* @optional {String} properties - A dot separated list of properties to observe
* @return {Array} - Array of observers
*/
_observeChildren: function( object, properties ) {
object = object || this.object;
var parts = ( properties ) ? properties.split( '.' ) : this.property.split( '.' ),
property = parts.shift(),
observers = [], observer;
var observeLastProperty = this.options.observeLastProperty;
// Use the right observe function ( sync/async
if( parts[ 0 ] === '@each' || property === '@each' ) {
observer = this._addEachObservers( object, property, parts );
}
else if( property === '@any' ) {
return this._addAnyObservers( parts.join( '.' ) );
}
else {
object[ property ] = object[ property ] || {};
// Observe the first child
observer = this.observeFunction.call( gms.Observe, object, property, this.callback, this.context, this.options );
}
// Add the observer to the array
observers.push( observer );
// Get recursive
if( parts.length > 1 ){
var childObservers = this._observeChildren( object[ property ], parts.join('.') );
observers = observers.concat( childObservers );
}
// Observe the last child
else {
if( property !== '@any' ) {
object = object[ property ];
}
property = parts[ 0 ];
// Clone the options object so we can set a property on it
var optionsClone = ( this.options !== undefined ) ? goog.object.clone( this.options ) : { };
if( observeLastProperty ){
optionsClone.lastProperty = true;
}
observer = this.observeFunction.call( gms.Observe, object, parts.join( '.' ), this.callback, this.context, optionsClone );
observers.push( observer );
}
return observers;
},
/**
* Adds an .@each observer to an array
* @param {Object} object - The object to observe
* @param {String} property - The property
* @param {Array} parts - An array of all the child observers
* @return {Observer} - The observer added
*/
_addEachObservers: function( object, property, parts ) {
var eachObserver = ( parts[ 0 ] === '@each' ) ? parts.slice( 1 ).join( '.' ) : parts.join( '.' );
object[ property ] = object[ property ] || [];
var observer = this._observeArray( object, property, eachObserver );
if( parts[ 0 ] === '@each' ){
parts.splice( 0, 1 );
}
return observer;
},
/**
* Adds an .@any observer to an object
* @param {String} property - The property
* @return {Array} - The observers added
*/
_addAnyObservers: function( property ) {
var observers = [];
if( this.sync && ! this.options.ignoreAnySyncWarning ){
console.warn( '----------------' );
console.log( 'You are synchronously observing @any on an object.' );
console.log( 'Your callback will fire when any property currently defined on the object changes' );
console.log( 'But will not fire when new properties are added.' );
console.log( 'To acheive both, you must observe @any asynchronously.' );
console.log( 'To suppress this message pass ignoreAnySyncWarning: true as an Observer options argument');
console.warn( '----------------' );
}
for( var prop in this.object ) {
var observer = this.observeFunction.call( gms.Observe, this.object, prop, this.callback, this.context, this.options );
observers.push( observer );
// If we're observing a property on @any item, do it here!
if( property !== '' && property !== '@any' ) {
observer = this.observeFunction.call( gms.Observe, this.object[ prop ], property, this.callback, this.context, this.options );
observers.push( observer );
}
}
return observers;
},
/**
* Creates the Observer object
* @return {Observer} - The Observer object
*/
_createObserver: function() {
// TODO: maybe make this its own class?
return {
callback : this.callback,
context : this.context,
sync : this.sync,
object : this.object,
property : this.property,
nativeObserve : this.useNative,
options : this.options
};
},
/**
* Creates the change object that gets returned to the callback
* @param {Object} object - The object listening for the change
* @param {String} property - The property changed
* @param {*} oldValue - The previous value
* @return {change} - the Change object to return to the callback
*/
_createChange: function( object, property, oldValue, args ) {
// TODO: maybe make this its own class?
return {
type : 'update',
object : object,
name : property,
oldValue : oldValue,
args : args,
currentValue : object[ property ]
};
},
_createArrayChange: function( array, item, type, oldValue, args ) {
var change = {
type : type,
object : array,
index : array.indexOf( item ),
oldValue : oldValue,
currentValue : array[ array.indexOf( item ) ],
args : args
};
if( type === 'delete' ) {
change.oldValue = item;
}
return change;
},
/**
* Takes a change object returned by the native Object.observe and adds some properties to it
* @param {Object} change - The change returned by O.o
* @return {Change} - The new change object
*/
_cloneNativeChange: function( change, args ) {
// TODO: maybe make this its own class?
return {
type : 'update',
object : change.object,
name : change.name,
oldValue : change.oldValue,
args : args,
currentValue : change.object[ change.name ]
};
},
/**
* Takes a change object returned by the native Object.observe( array ) and adds some properties to it
* @param {Object} change - The change returned by O.o
* @return {Change} - The new change object
*/
_cloneNativeArrayChange: function( change, args ) {
// TODO: maybe make this its own class?
return {
type : change.type,
object : change.object,
index : change.name,
oldValue : change.oldValue,
args : args,
currentValue : change.object[ change.name ]
};
},
/**
**** Native Object.observe Methods *******
-----------------------------------
*/
/**
* Uses the native Object.observe
*/
_addObjectObserver: function() {
var observedProperty = this.property,
_this = this;
Object.observe( this.object, function( changes ) {
// We only care if an object has been updated, added, or deleted
changes = changes.filter( function( c ) {
return [ 'add', 'update', 'delete' ].indexOf( c.type ) > -1;
});
_this._objectObserverCallback( changes, observedProperty );
});
},
/**
* Users the native Object.observe to observe changes to an array
* @param {Array} array - The array to observe
*/
_addArrayObjectObserver: function( array ) {
var _this = this;
Object.observe( array, function( change ) {
_this._arrayObjectObserverCallback( change );
});
},
/**
* The callback fired by the native Object.observe
* @param {Object} changed - The change object
* @param {String} observedProperty - The property observed
* @return {null}
*/
_objectObserverCallback: function( changed, observedProperty ) {
changed.forEach( function( change ) {
this._forEachChangedObject( change, observedProperty );
}, this );
},
/**
* Callback fired by the array Object.observe
* @param {Array} changed - Array of changes
* @return {null}
*/
_arrayObjectObserverCallback: function( changed ) {
changed.forEach( function( change ) {
this._forEachArrayChangedObject( change );
}, this );
},
/**
* Looks through all the changed objects for one matching our property
* @param {[type]} change [description]
* @return {[type]} [description]
*/
_forEachChangedObject: function( change, observedProperty ) {
var property = observedProperty,
object = change.object,
observers = object.__observers__;
if( change.name === property || observedProperty === '@any' ) {
observers[ property ].forEach( function( observer ){
change = this._cloneNativeChange( change, observer.options );
gms.Observe._onObservedPropChange( observer, change );
}, this );
}
},
/**
* Handles adding array changes to the observer queue
* Only adds to queue when an actual item in the array has changed, not an array property ( like length )
* @param {Change} change - A single change object
* @return {null}
*/
_forEachArrayChangedObject: function( change ) {
var array = change.object,
observers = array.__observers__;
if( ! isNaN( change.name ) ) {
this._reattachEachObservers( array, change.object[ change.name ] );
observers.forEach( function( observer ){
change = this._cloneNativeArrayChange( change, observer.options );
gms.Observe._onObservedPropChange( observer, change );
}, this );
}
},
/**
**** Non-Native Observer Methods ****
*------------------------------------
*/
/**
* Manually creates setters and getter functions
*/
_addSetters: function( ){
var object = this.object,
property = this.property;
if( property === '@any' ) {
return this._addAnyObservers( this.property );
}
// Check if __ private __ property is defined
if( ! object.hasOwnProperty( '__' + property + '__' ) ) {
this._addPrivateProp( object, property );
// Then redefine the propertyerty
var setter = object.__lookupSetter__( property ),
_this = this;
Object.defineProperty( object, property, {
configurable: true,
get: function( ) {
return this[ '__' + property + '__' ];
},
set: function( value ) {
var oldValue = this[ property ];
if( value !== oldValue ) {
if( setter ) {
setter( value );
} else {
object[ '__' + property + '__' ] = value;
}
return _this._set.call( _this, this, property, oldValue );
}
}
});
}
},
/**
* Function called after an observed property is changed, creates a queue of callbacks to fire
* @param {Object} object The observed object
* @param {String} property The changed property name
* @param {*} oldValue The previous value of the property
*/
_set: function( object, property, oldValue ) {
if( object.__observers__ && object.__observers__[ property ] ) {
object.__observers__[ property ].forEach( function( observer ){
var changed = this._createChange( object, property, oldValue, observer.options );
gms.Observe._onObservedPropChange( observer, changed );
}, this );
}
},
/**
* Creates proxies for array methods push, splice, pop and sort; which trigger _onArrayUpdated when called
* @param {Array} array - The array to add the methods to
*/
_addArrayMethods: function( array ) {
var _this = this;
array.push = function( ) {
var pushed = Array.prototype.push.apply( this, arguments );
_this._onArrayUpdated( this, 'add', Array.prototype.slice.call( arguments, [ this[ pushed ] ] ) );
return pushed;
};
array.splice = function( ) {
var spliced = Array.prototype.splice.apply( this, arguments );
if( arguments[ 2 ] !== undefined ){
_this._onArrayUpdated( this, 'add', [ arguments[ 2 ] ] );
} else {
_this._onArrayUpdated( this, 'delete', spliced );
}
return spliced;
};
array.pop = function( ) {
var popped = Array.prototype.pop.apply( this, arguments );
_this._onArrayUpdated( this, 'delete', popped );
return popped;
};
array.sort = function( ) {
var sorted = Array.prototype.sort.apply( this, arguments );
_this._onArrayUpdated( this, 'change', sorted );
return sorted;
};
},
/**
* Called by the proxied array methods
* @param {Array} array - The array updated
* @param {String} type - The type of update [ 'add', 'delete', 'update' ]
* @param {Array} changedItems - The items that were added, deleted, or updated
* @return {null}
*/
_onArrayUpdated: function( array, type, changedItems ) {
if( array['__observers__'] ) {
// If we're observing properties on each array item
// Observe them on the new items we're adding
if( type === 'add' && array.__eachObservers__ ){
this._reattachEachObservers( array, changedItems );
}
for( var i = 0, l = array['__observers__'].length; i < l; i++ ) {
var observer = array['__observers__'][ i ];
var changes = [];
if( changedItems instanceof Array ) {
changedItems.forEach( function( item ) {
var change = this._createArrayChange( array, item, type, observer.options );
changes.push( change );
}, this );
}
else {
changes.push( this._createArrayChange( array, changedItems, type, observer.options ) );
}
gms.Observe._onObservedPropChange( observer, changes );
}
}
},
/**
* Reattaches the each observer to items in an array
* @param {Array} array - The array
* @param {Array} items - An array of items to attach
* @return {null}
*/
_reattachEachObservers: function( array, items ) {
for( var observedProp in array.__eachObservers__ ){
var observers = array.__eachObservers__[ observedProp ];
observers.forEach( function( observer ){
if( items instanceof Array ){
items.forEach( function( arrayItem ){
this.observeFunction.call( gms.Observe, arrayItem, observedProp, observer.callback, observer.context, observer.args );
}, this );
}
else {
this.observeFunction.call( gms.Observe, items, observedProp, observer.callback, observer.context, observer.args );
}
}, this );
}
},
/**
* Creates the internal __property__ on the object
* @param {Object} object The object
* @param {String} property The property
*/
_addPrivateProp: function( object, property ) {
var getter = object.__lookupGetter__( property );
if( getter ) {
Object.defineProperty( object, '__' + property + '__', {
enumerable : false,
get : getter
});
}
else {
Object.defineProperty( object, '__' + property + '__', {
enumerable : false,
value : object[ property ],
writable : true
});
}
},
_observerAlreadyAttached: function( ) {
if( this.object.__observers__ && this.object.__observers__[ this.property ] ){
return this.object.__observers__[ this.property ].filter( function( observer ) {
return this.callback === observer.callback && this.context === observer.context;
}, this );
}
return false;
}
};
gms.Observe = {
// The callback queue
_queue: [],
/**
* Observes a single property
* @param {Object} object - The object to observe
* @param {String} property - The name of the property
* @param {Function} callback - The callback function
* @param {Object} context - The context to run the callback
* @param {Object} options - Optional options
* @return {Observer} - The Observer object
*/
property: function( object, property, callback, context, options ) {
if( typeof property !== 'string' ) {
if( Array.isArray( property ) ){
return this.properties.apply( this, arguments );
}
throw 'You must pass a string to Observe.property';
}
return new gms.Observer( object, property, callback, context, options );
},
/**
* Observe a single property synchronously
* @param {Object} object - The object to observe
* @param {String} property - The property to observe
* @param {Function} callback - The callback function
* @param {Object} context - The context to run the callback
* @param {Object} options - Optional options
* @return {Observer} - The observer object
*/
propertySync: function( object, property, callback, context, options ) {
options = options || {};
options.sync = true;
return this.property( object, property, callback, context, options );
},
/**
* Observe multiple properties on an object
* @param {Object} object - The object to observer
* @param {Array} properties - An array of properties
* @param {Function} callback - The callback to fire
* @param {Object} context - The context to run the callback
* @param {Object} options - Optional options
* @return {Array} - The created observers
*/
properties: function( object, properties, callback, context, options ) {
if( ! ( properties instanceof Array ) ) {
throw 'You must pass an array to Observe.properties';
}
var observers = [];
properties.forEach( function( property ){
observers.push( this.property( object, property, callback, context, options ) );
}, this );
return observers;
},
/**
* Observe multiple properties on an object synchronously
* @param {Object} object - The object to observer
* @param {Array} properties - An array of properties
* @param {Function} callback - The callback to fire
* @param {Object} context - The context to run the callback
* @param {Object} options - Optional options
* @return {Array} - The created observers
*/
propertiesSync: function( object, properties, callback, context, options ) {
var observers = [];
properties.forEach( function( property ){
observers.push( this.propertySync( object, property, callback, context, options ) );
}, this );
return observers;
},
/**
* Creates observers to fire the observer only when the last property in the chain changes
* @param {Object} object - The object to observe
* @param {String} property - The property
* @param {Function} callback - The callback
* @param {Object} context - The context to run the callback with
* @param {Object} options - Optional options
* @return {Observer} The Observer
*/
lastProperty: function( object, property, callback, context, options ) {
options = options || {};
options.observeLastProperty = true;
this.property( object, property, callback, context, options );
},
/**
* Creates observers to fire the observer synchronously only when the last property in the chain changes
* @param {Object} object - The object to observe
* @param {String} property - The property
* @param {Function} callback - The callback
* @param {Object} context - The context to run the callback with
* @param {Object} options - Optional options
* @return {Observer} The Observer
*/
lastPropertySync: function( object, property, callback, context, options ) {
options = options || {};
options.observeLastProperty = true;
this.propertySync( object, property, callback, context, options );
},
/**
* Stop observing a property
* @param {Observer|Array} observer - The observer(s) to stop
* @return {null}
*/
stop: function( observer ) {
if( observer instanceof Array ) {
observer.forEach( this._removeObserver, this );
} else {
this._removeObserver( observer );
}
},
/**
* Remove an observer from an object
* @param {Object} object - The object
* @param {String} property - The property to stop observing
* @param {Function} callback - The callback
* @param {Object} context - The context the callback runs in
* @return {null}
*/
remove: function( object, property, callback, context ) {
var observers = object.__observers__,
// Find the observers to remove
observersToRemove = observers[ property ].filter( function( o ) {
return o.callback === callback && o.context === context;
});
this.stop( observersToRemove );
},
/**
* Pause an observer
* @param {Observer} observer - The observer to pause
*/
pause: function( observer ) {
observer.paused = true;
},
/**
* Resume a paused observer
* @param {Observer} observer - The observer to resume
* @param {Boolean} fireLastChange - Whether to fire the callback for the last change
*/
resume: function( observer, fireLastChange ) {
observer.paused = false;
if( fireLastChange && observer.__lastChange__ ) {
this._onObservedPropChange( observer, observer.__lastChange__ );
}
delete observer.__lastChange__;
},
removeAllFromObject: function( object ) {
for( var prop in object.__observers__ ){
object.__observers__[ prop ].forEach( this._removeObserver, this );
}
},
/**
* Adds an observer to the queue and starts resets a timer to fire the queue
* @param {Observer} observer - The Observer to add
* @param {Array} changes - Array of changes
* @return {null}
*/
_pushObserverToQueue: function( observer, changes ) {
var item = this._queue.find( function( item ){
return item.callback === observer.callback && item.context === observer.context;
} );
if( item ) {
item.changes = item.changes.concat( changes );
} else {
this._queue.push( {
callback : observer.callback,
context : observer.context,
changes : changes
});
}
window.clearTimeout( this._queueTimer );
this._queueTimer = window.setTimeout( this._fireQueuedCallbacks.bind( this ), 2 );
},
/**
* Fires the queued callbacks from the changed/setters
* @return {null}
*/
_fireQueuedCallbacks: function() {
for( var i = 0, l = this._queue.length; i < l; i++ ) {
var item = this._queue.shift();
if( item ) {
item.callback.call( item.context, item.changes, item.options );
}
}
// If things got added while we were running the _queue, run it again!
if( this._queue.length > 0 ) {
this._fireQueuedCallbacks();
}
},
/**
* Fired when an observed property changes
* Handles reattaching child observers and queuing/firing callbacks
* @param {Observer} observer - The observer that changed
* @param {Array|Object} change - The change object(s)
* @return {null}
*/
_onObservedPropChange: function( observer, change ) {
// Always make changes an array
var changes = ( change instanceof Array ) ? change: [ change ];
this._reattachOnChange( changes );
if( observer.paused ) {
observer.__lastChange__ = change;
return;
}
if( observer.options && observer.options.observeLastProperty && ! observer.options.lastProperty ) {
return;
}
if( observer.sync ){
observer.callback.call( observer.context, changes, observer.options );
} else {
gms.Observe._pushObserverToQueue( observer, changes );
}
},
/**
* Reattaches all child observers to a changed object
* @param {Array} changes - Array of changes
* @return {null}
*/
_reattachOnChange: function( changes ) {
changes.forEach( function( change ){
this._reattachChildObservers( change.object, change.name, change.oldValue );
}, this );
},
_reattachChildObservers: function( object, property, oldValue ) {
if( typeof object !== 'object' || object === null ) {
return;
}
if( !oldValue || oldValue.__observers__ === undefined ) {
return;
}
var observersToRemove = [];
// Loop through all the old observers
for( var observedProp in oldValue.__observers__ ) {
var observers = oldValue.__observers__[ observedProp ];
// Reattach them all
observers.forEach( function( observer ) {
if( observer.context === oldValue ) {
observer.context = object[ property ];
}
// Use the right observe function ( sync/async )
var observeFunction = ( observer.sync ) ? this.propertySync : this.property;
// Reattach the observers to this property
observeFunction.call( this, object[ property ], observedProp, observer.callback, observer.context );
// Add this observer to the list to remove
observersToRemove.push( observer );
}, this );
this._reattachChildObservers( object[ property ], observedProp, oldValue[ observedProp ] );
}
// Remove all the old observers
observersToRemove.forEach( this._removeObserver, this );
},
/**
* Removes a single observer
* @param {Observer} observer - The observer to remove
* @return {null}
*/
_removeObserver: function( observer ) {
if( ! observer ) {
return;
}
var object = observer.object,
property = observer.property;
if( ! object ) {
return;
}
var observerIndex = object.__observers__[ property ].findIndex( function( o ) {
return o === observer;
});
object.__observers__[ property ].splice( observerIndex, 1 );
// If there are no observers left, stop observing the object
var hasNoKeys = Object.keys( object.__observers__ ).length === 0;
if( hasNoKeys ) {
if( observer.nativeObserve ) {
Object.unobserve( observer.object, observer.callback );
}
}
},
};
var object = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5
};
var changes;
var callbackFunction = function( changes ) {
changes = changes;
console.log( changes );
//debugger;
deferred.resolve();
}
Ready to run.
Test | Ops/sec | |
---|---|---|
Object.observe |
| ready |
Getters-setters |
| ready |
You can edit these tests or add more tests to this page by appending /edit to the URL.