Moved test-class-abstract into suite as Class/AbstractTest
parent
37d66b375d
commit
aad013bd87
|
@ -0,0 +1,534 @@
|
||||||
|
/**
|
||||||
|
* Tests abstract classes
|
||||||
|
*
|
||||||
|
* Copyright (C) 2010, 2011, 2012, 2013 Mike Gerwitz
|
||||||
|
*
|
||||||
|
* This file is part of GNU ease.js.
|
||||||
|
*
|
||||||
|
* ease.js is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require( 'common' ).testCase(
|
||||||
|
{
|
||||||
|
caseSetUp: function()
|
||||||
|
{
|
||||||
|
this.Sut = this.require( 'class_abstract' );
|
||||||
|
this.Class = this.require( 'class' );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to ensure that the code documents itself, we should require
|
||||||
|
* that all classes containing abstract members must themselves be
|
||||||
|
* declared as abstract. Otherwise, you are at the mercy of the
|
||||||
|
* developer's documentation/comments to know whether or not the class
|
||||||
|
* is indeed abstract without looking through its definition.
|
||||||
|
*/
|
||||||
|
'Must declare classes with abstract members as abstract': function()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// should fail; class not declared as abstract
|
||||||
|
this.Class( 'Foo',
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
this.assertOk(
|
||||||
|
e.message.search( 'Foo' ) !== -1,
|
||||||
|
"Abstract class declaration error should contain class name"
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assertFail(
|
||||||
|
"Should not be able to declare abstract members unless " +
|
||||||
|
"class is also declared as abstract"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract members should be permitted if the class itself is declared
|
||||||
|
* as abstract; converse of above test.
|
||||||
|
*/
|
||||||
|
'Can declare class as abstract': function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Sut(
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
} );
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a class is declared as abstract, it should contain at least one
|
||||||
|
* abstract method. Otherwise, the abstract definition is pointless and
|
||||||
|
* unnecessarily confusing---the whole point of the declaration is
|
||||||
|
* to produce self-documenting code.
|
||||||
|
*/
|
||||||
|
'Abstract classes must contain abstract methods': function()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// should fail; class not declared as abstract
|
||||||
|
this.Sut( 'Foo', {} );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
this.assertOk(
|
||||||
|
e.message.search( 'Foo' ) !== -1,
|
||||||
|
"Abstract class declaration error should contain class name"
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assertFail(
|
||||||
|
"Abstract classes should contain at least one abstract method"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract methods should remain virtual until they are defined.
|
||||||
|
* That is, if a subtype doesn't provide a concrete implementation, it
|
||||||
|
* should still be considered virtual.
|
||||||
|
*/
|
||||||
|
'Abstract methods can be defined concretely by sub-subtypes': function()
|
||||||
|
{
|
||||||
|
var AbstractFoo = this.Sut( 'Foo',
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
} ),
|
||||||
|
|
||||||
|
SubAbstractFoo = this.Sut.extend( AbstractFoo, {} );
|
||||||
|
|
||||||
|
var Class = this.Class;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Class.extend( SubAbstractFoo,
|
||||||
|
{
|
||||||
|
// we should NOT need the override keyword for concrete
|
||||||
|
// implementations of abstract super methods
|
||||||
|
'foo': function() {},
|
||||||
|
} )
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just as Class contains an extend method, so should AbstractClass.
|
||||||
|
*/
|
||||||
|
'Abstract class extend method returns new class': function()
|
||||||
|
{
|
||||||
|
this.assertEqual( typeof this.Sut.extend, 'function',
|
||||||
|
"AbstractClass contains extend method"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.assertOk(
|
||||||
|
this.Class.isClass(
|
||||||
|
this.Sut.extend( { 'abstract foo': [] } )
|
||||||
|
),
|
||||||
|
"Abstract class extend method returns class"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just as Class contains an implement method, so should AbstractClass.
|
||||||
|
* We test implementation further on in this test case.
|
||||||
|
*/
|
||||||
|
'Abstract class contains implement method': function()
|
||||||
|
{
|
||||||
|
this.assertEqual( typeof this.Sut.implement, 'function',
|
||||||
|
"AbstractClass contains implement method"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All classes should have a method to determine if they are abstract.
|
||||||
|
* We test specific cases below.
|
||||||
|
*/
|
||||||
|
'All classes have an isAbstract() method': function()
|
||||||
|
{
|
||||||
|
this.assertEqual(
|
||||||
|
typeof ( this.Class( {} ).isAbstract ),
|
||||||
|
'function'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For this test, note that (as was tested above) a class containing
|
||||||
|
* abstract members must be declared as abstract; therefore, this test
|
||||||
|
* extends to assert that classes with no abstract methods are not
|
||||||
|
* considered to be abstract.
|
||||||
|
*/
|
||||||
|
'Concrete classes are not considered to be abstract': function()
|
||||||
|
{
|
||||||
|
this.assertOk( !( this.Class( {} ).isAbstract() ) );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the same spirit as the preceding test, this extends to asserting
|
||||||
|
* that a class containing abstract methods must be considered to be
|
||||||
|
* abstract.
|
||||||
|
*/
|
||||||
|
'Abstract classes are considered to be abstract': function()
|
||||||
|
{
|
||||||
|
this.assertOk(
|
||||||
|
this.Sut( { 'abstract method': [] } ).isAbstract()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the spirit of the aforementioned, subtypes that do not provide
|
||||||
|
* concrete definitions for *all* abstract methods of their supertype
|
||||||
|
* must too be considered to be abstract.
|
||||||
|
*/
|
||||||
|
'Subtypes are abstract if no concrete method is provided': function()
|
||||||
|
{
|
||||||
|
var Base = this.Sut(
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
'abstract bar': [],
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.assertOk(
|
||||||
|
this.Sut.extend( Base,
|
||||||
|
{
|
||||||
|
// only provide concrete impl. for a single method; `bar' is
|
||||||
|
// still abstract
|
||||||
|
foo: function() {}
|
||||||
|
} ).isAbstract()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that a subtype is not considered to be abstract if it provides
|
||||||
|
* concrete definitions of each of its supertype's abstract methods.
|
||||||
|
*/
|
||||||
|
'Subtypes are not considered abstract if concrete methods are provided':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Base = this.Sut(
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
'abstract bar': [],
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.assertOk(
|
||||||
|
this.Class.extend( Base,
|
||||||
|
{
|
||||||
|
// provide concrete impls. for both
|
||||||
|
foo: function() {},
|
||||||
|
bar: function() {},
|
||||||
|
} ).isAbstract() === false
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since an abstract class does not provide a complete object
|
||||||
|
* description, it cannot be instantiated.
|
||||||
|
*/
|
||||||
|
'Abstract classes cannot be instantiated': function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
Sut( { 'abstract foo': [] } )();
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* However, a concrete subtype of an abstract class may be instantiated.
|
||||||
|
* Otherwise abstract classes would be pretty useless.
|
||||||
|
*/
|
||||||
|
'Concrete subtypes of abstract classes can be instantiated': function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Sut( { 'abstract foo': [] } )
|
||||||
|
.extend( { foo: function() {} } )
|
||||||
|
();
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Even though an abstract class itself cannot be instantiated, its
|
||||||
|
* constructor may still be inherited (and therefore invoked) through
|
||||||
|
* concrete subtypes.
|
||||||
|
*/
|
||||||
|
'Can call constructors of abstract supertypes': function()
|
||||||
|
{
|
||||||
|
var ctor_called = false;
|
||||||
|
this.Sut(
|
||||||
|
{
|
||||||
|
__construct: function() { ctor_called = true; },
|
||||||
|
'abstract foo': [],
|
||||||
|
} ).extend( { foo: function() {} } )();
|
||||||
|
|
||||||
|
this.assertOk( ctor_called );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract methods declare an API strictly for the purpose of ensuring
|
||||||
|
* that subtypes are all compatible with respect to that particular
|
||||||
|
* field; parameter count, therefore, should be enforced to point out
|
||||||
|
* potential bugs to developers. Whether or not the subtype makes use of
|
||||||
|
* a particular argument is a separate and unrelated issue.
|
||||||
|
*/
|
||||||
|
'Concrete methods must implement the proper number of parameters':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// concrete implementation missing parameter `two'
|
||||||
|
Sut( { 'abstract foo': [ 'one', 'two' ] } )
|
||||||
|
.extend( { foo: function( one ) {} } );
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It may be the case that a subtype wishes to provide a new definition
|
||||||
|
* for a particular abstract method---without providing a concrete
|
||||||
|
* implementation---to add additional parameters. However, to remain
|
||||||
|
* compatible with the supertype, that implementation must provide at
|
||||||
|
* least the same number of arguments as the respective method of the
|
||||||
|
* supertype.
|
||||||
|
*
|
||||||
|
* This tests the error condition; see below for the complement.
|
||||||
|
*/
|
||||||
|
'Abstract methods of subtypes must declare compatible parameter count':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
Sut.extend( Sut( { 'abstract foo': [ 'one' ] } ),
|
||||||
|
{
|
||||||
|
// incorrect number of arguments
|
||||||
|
'abstract foo': [],
|
||||||
|
} );
|
||||||
|
}, TypeError );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complements the above test to ensure that compatible abstract
|
||||||
|
* overrides are permitted.
|
||||||
|
*/
|
||||||
|
'Abstract members may implement more parameters than supertype':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Sut.extend( Sut( { 'abstract foo': [ 'one' ] } ),
|
||||||
|
{
|
||||||
|
// one greater
|
||||||
|
'abstract foo': [ 'one', 'two' ],
|
||||||
|
} );
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* While this may not necessarily be sensical in all situations, it may
|
||||||
|
* be useful for documentation.
|
||||||
|
*/
|
||||||
|
'Abstract members may implement equal parameters to supertype':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Sut.extend( Sut( { 'abstract foo': [ 'one' ] } ),
|
||||||
|
{
|
||||||
|
// same number
|
||||||
|
'abstract foo': [ 'one' ],
|
||||||
|
} );
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test just ensures consistency by ensuring that an empty
|
||||||
|
* parameter definition for abstract methods imposes no parameter count
|
||||||
|
* requirement on its concrete definition.
|
||||||
|
*/
|
||||||
|
'Concrete methods have no parameter requirement with empty definition':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Sut( { 'abstract foo': [] } ).extend(
|
||||||
|
{
|
||||||
|
foo: function() {}
|
||||||
|
} );
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract method is represented by an array listing its parameters
|
||||||
|
* (that must be implemented by concrete definitions).
|
||||||
|
*/
|
||||||
|
'Abstract methods must be declared as arrays': function()
|
||||||
|
{
|
||||||
|
var Class = this.Class;
|
||||||
|
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// likely demonstrates misunderstanding of the syntax
|
||||||
|
Class.extend( { 'abstract foo': function() {} } );
|
||||||
|
}, TypeError, "Abstract method cannot be declared as a function" );
|
||||||
|
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// might be common mistake for attempting to denote a single
|
||||||
|
// parameter; pure speculation.
|
||||||
|
Class.extend( { 'abstract foo': 'scalar' } );
|
||||||
|
}, TypeError, "Abstract method cannot be declared as a scalar" );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There was an issue where the object holding the abstract methods list
|
||||||
|
* was not checking for methods by using hasOwnProperty(). Therefore, if
|
||||||
|
* a method such as toString() was defined, it would be matched in the
|
||||||
|
* abstract methods list. As such, the abstract methods count would be
|
||||||
|
* decreased, even though it was not an abstract method to begin with
|
||||||
|
* (nor was it removed from the list, because it was never defined in
|
||||||
|
* the first place outside of the prototype).
|
||||||
|
*
|
||||||
|
* This negative number !== 0, which causes a problem when checking to
|
||||||
|
* ensure that there are 0 abstract methods. We check explicitly for 0
|
||||||
|
* because, if it's non-zero, then it's either abstract or something is
|
||||||
|
* wrong. Negative is especially wrong. It should never be negative!
|
||||||
|
*/
|
||||||
|
'Does not recognize object prototype members as abstract': function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut;
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
Sut( { 'abstract method': [] } ).extend(
|
||||||
|
{
|
||||||
|
// concrete, so the result would otherwise not be abstract
|
||||||
|
method: function() {},
|
||||||
|
|
||||||
|
// the problem---this exists in the prototype chain of every
|
||||||
|
// object
|
||||||
|
'toString': function() {},
|
||||||
|
})();
|
||||||
|
}, Error );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure we support named abstract class extending
|
||||||
|
*/
|
||||||
|
'Can create named abstract subtypes': function()
|
||||||
|
{
|
||||||
|
this.assertOk(
|
||||||
|
this.Sut( 'Named' ).extend(
|
||||||
|
this.Sut( { 'abstract foo': [] } ),
|
||||||
|
{}
|
||||||
|
).isAbstract()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract classes, when extended, should yield a concrete class by
|
||||||
|
* default. Otherwise, the user should once again use AbstractClass to
|
||||||
|
* clearly state that the subtype is abstract. Remember:
|
||||||
|
* self-documenting.
|
||||||
|
*/
|
||||||
|
'Calling extend() on abstract class yields concrete class': function()
|
||||||
|
{
|
||||||
|
var Foo = this.Sut( { 'abstract foo': [] } ),
|
||||||
|
cls_named = this.Sut( 'NamedSubFoo' ).extend( Foo, {} ),
|
||||||
|
cls_anon = this.Sut.extend( Foo, {} );
|
||||||
|
|
||||||
|
var Class = this.Class;
|
||||||
|
|
||||||
|
// named
|
||||||
|
this.assertThrows(
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
// should throw an error, since we're not declaring it as
|
||||||
|
// abstract and we're not providing a concrete impl
|
||||||
|
Class.isAbstract( cls_named.extend( {} ) );
|
||||||
|
},
|
||||||
|
TypeError,
|
||||||
|
"Extending named abstract classes should be concrete"
|
||||||
|
);
|
||||||
|
|
||||||
|
// anonymous
|
||||||
|
this.assertThrows(
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
// should throw an error, since we're not declaring it as abstract
|
||||||
|
// and we're not providing a concrete impl
|
||||||
|
Class.isAbstract( cls_anon.extend( {} ) );
|
||||||
|
},
|
||||||
|
TypeError,
|
||||||
|
"Extending anonymous abstract classes should be concrete"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extending an abstract class after an implement() should still result
|
||||||
|
* in an abstract class. Essentially, we are testing to ensure that the
|
||||||
|
* extend() method is properly wrapped to flag the resulting class as
|
||||||
|
* abstract. This was a bug.
|
||||||
|
*/
|
||||||
|
'Implementing interfaces will preserve abstract class definition':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var Sut = this.Sut,
|
||||||
|
Interface = this.require( 'interface' );
|
||||||
|
|
||||||
|
this.assertOk(
|
||||||
|
// if not considered abstract, extend() will fail, as it will
|
||||||
|
// contain abstract member foo
|
||||||
|
Sut( 'TestImplExtend' )
|
||||||
|
.implement( Interface( { foo: [] } ) )
|
||||||
|
.extend( {} )
|
||||||
|
.isAbstract()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} );
|
|
@ -1,468 +0,0 @@
|
||||||
/**
|
|
||||||
* Tests abstract classes
|
|
||||||
*
|
|
||||||
* Copyright (C) 2010, 2011, 2012, 2013 Mike Gerwitz
|
|
||||||
*
|
|
||||||
* This file is part of GNU ease.js.
|
|
||||||
*
|
|
||||||
* ease.js is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var common = require( './common' ),
|
|
||||||
assert = require( 'assert' ),
|
|
||||||
util = common.require( 'util' ),
|
|
||||||
|
|
||||||
Class = common.require( 'class' ),
|
|
||||||
AbstractClass = common.require( 'class_abstract' ),
|
|
||||||
Interface = common.require( 'interface' )
|
|
||||||
;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In order to ensure the code documents itself, we should require that all
|
|
||||||
* classes containing abstract members must themselves be declared as abstract.
|
|
||||||
* Otherwise, you are at the mercy of the developer's documentation/comments to
|
|
||||||
* know whether or not the class is indeed abstract without looking through its
|
|
||||||
* definition.
|
|
||||||
*/
|
|
||||||
( function testMustDeclareClassesWithAbstractMembersAsAbstract()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// should fail; class not declared as abstract
|
|
||||||
Class( 'Foo',
|
|
||||||
{
|
|
||||||
'abstract foo': [],
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
catch ( e )
|
|
||||||
{
|
|
||||||
assert.ok(
|
|
||||||
e.message.search( 'Foo' ) !== -1,
|
|
||||||
"Abstract class declaration error should contain class name"
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.fail(
|
|
||||||
"Should not be able to declare abstract members unless class is also " +
|
|
||||||
"declared as abstract"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract members should be permitted if the class itself is declared as
|
|
||||||
* abstract
|
|
||||||
*/
|
|
||||||
( function testCanDeclareClassAsAbstract()
|
|
||||||
{
|
|
||||||
AbstractClass(
|
|
||||||
{
|
|
||||||
'abstract foo': [],
|
|
||||||
} );
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a class is declared as abstract, it should contain at least one abstract
|
|
||||||
* method. Otherwise, the abstract definition is pointless and unnecessarily
|
|
||||||
* confusing. The whole point of the declaration is self-documenting code.
|
|
||||||
*/
|
|
||||||
( function testAbstractClassesMustContainAbstractMethods()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// should fail; class not declared as abstract
|
|
||||||
AbstractClass( 'Foo', {} );
|
|
||||||
}
|
|
||||||
catch ( e )
|
|
||||||
{
|
|
||||||
assert.ok(
|
|
||||||
e.message.search( 'Foo' ) !== -1,
|
|
||||||
"Abstract class declaration error should contain class name"
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.fail(
|
|
||||||
"Abstract classes should contain at least one abstract method"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract methods should remain virtual until they are overridden. That is, if
|
|
||||||
* a subtype doesn't provide a concrete implementation, it should still be
|
|
||||||
* considered virtual.
|
|
||||||
*/
|
|
||||||
( function testAbstractMethodsCanBeOverriddenBySubSubTypes()
|
|
||||||
{
|
|
||||||
var AbstractFoo = AbstractClass( 'Foo',
|
|
||||||
{
|
|
||||||
'abstract foo': [],
|
|
||||||
} ),
|
|
||||||
|
|
||||||
SubAbstractFoo = AbstractClass.extend( AbstractFoo, {} ),
|
|
||||||
|
|
||||||
ConcreteFoo = Class.extend( SubAbstractFoo,
|
|
||||||
{
|
|
||||||
// we should NOT need the override keyword for concrete
|
|
||||||
// implementations of abstract super methods
|
|
||||||
'foo': function() {},
|
|
||||||
} )
|
|
||||||
;
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Just as Class contains an extend method, so should AbstractClass.
|
|
||||||
*/
|
|
||||||
( function testAbstractClassExtendMethodReturnsNewClass()
|
|
||||||
{
|
|
||||||
assert.ok( typeof AbstractClass.extend === 'function',
|
|
||||||
"AbstractClass contains extend method"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.ok(
|
|
||||||
Class.isClass(
|
|
||||||
AbstractClass.extend( { 'abstract foo': [] } )
|
|
||||||
),
|
|
||||||
"Abstract class extend method returns class"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Just as Class contains an implement method, so should AbstractClass.
|
|
||||||
*/
|
|
||||||
( function testAbstractClassContainsImplementMethod()
|
|
||||||
{
|
|
||||||
assert.ok( typeof AbstractClass.implement === 'function',
|
|
||||||
"AbstractClass contains implement method"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
// not abstract
|
|
||||||
var Foo = Class.extend( {} );
|
|
||||||
|
|
||||||
// abstract (ctor_called is not a class member to ensure that visibility bugs do
|
|
||||||
// not impact our test)
|
|
||||||
var ctor_called = false,
|
|
||||||
AbstractFoo = AbstractClass.extend(
|
|
||||||
{
|
|
||||||
__construct: function()
|
|
||||||
{
|
|
||||||
ctor_called = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
'abstract method': [ 'one', 'two', 'three' ],
|
|
||||||
|
|
||||||
'abstract second': [],
|
|
||||||
})
|
|
||||||
;
|
|
||||||
|
|
||||||
// still abstract (didn't provide a concrete implementation of both abstract
|
|
||||||
// methods)
|
|
||||||
var SubAbstractFoo = AbstractClass.extend( AbstractFoo,
|
|
||||||
{
|
|
||||||
second: function()
|
|
||||||
{
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// concrete
|
|
||||||
var ConcreteFoo = Class.extend( AbstractFoo,
|
|
||||||
{
|
|
||||||
method: function( one, two, three )
|
|
||||||
{
|
|
||||||
// prevent Closure Compiler from optimizing the arguments away, causing
|
|
||||||
// a definition failure
|
|
||||||
return [ one, two, three ];
|
|
||||||
},
|
|
||||||
|
|
||||||
second: function()
|
|
||||||
{
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All classes should have a method to determine if they are abstract.
|
|
||||||
*/
|
|
||||||
( function testAllClassesHaveAMethodToDetmineIfAbstract()
|
|
||||||
{
|
|
||||||
assert.ok(
|
|
||||||
( Class( {} ).isAbstract instanceof Function ),
|
|
||||||
"All classes should have an isAbstract() method"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testClassesAreNotConsideredToBeAbstractIfTheyHaveNoAbstractMethods()
|
|
||||||
{
|
|
||||||
assert.equal(
|
|
||||||
Class( {} ).isAbstract(),
|
|
||||||
false,
|
|
||||||
"Classes are not abstract if they contain no abstract methods"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testClassesShouldBeConsideredAbstractIfTheyContainAbstractMethods()
|
|
||||||
{
|
|
||||||
assert.equal(
|
|
||||||
AbstractFoo.isAbstract(),
|
|
||||||
true,
|
|
||||||
"Classes should be considered abstract if they contain any abstract methods"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testSubtypesAreAbstractIfNoConcreteMethodIsProvided()
|
|
||||||
{
|
|
||||||
assert.equal(
|
|
||||||
SubAbstractFoo.isAbstract(),
|
|
||||||
true,
|
|
||||||
"Subtypes of abstract types are abstract if they don't provide a " +
|
|
||||||
"concrete implementation for all abstract methods"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testSubtypesAreNotConisderedAbstractIfConcreteImplIsProvided()
|
|
||||||
{
|
|
||||||
assert.equal(
|
|
||||||
ConcreteFoo.isAbstract(),
|
|
||||||
false,
|
|
||||||
"Subtypes of abstract types are not abstract if they provide concrete " +
|
|
||||||
"implementations of all abstract methods"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testAbstractClassesCannotBeInstantiated()
|
|
||||||
{
|
|
||||||
assert['throws']( function()
|
|
||||||
{
|
|
||||||
// both should fail
|
|
||||||
AbstractFoo();
|
|
||||||
SubAbstractFoo();
|
|
||||||
}, Error, "Abstract classes cannot be instantiated" );
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testConcreteSubclassesCanBeInstantiated()
|
|
||||||
{
|
|
||||||
assert.ok(
|
|
||||||
ConcreteFoo(),
|
|
||||||
"Concrete subclasses can be instantiated"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testCanCallConstructorsOfAbstractSupertypes()
|
|
||||||
{
|
|
||||||
ctor_called = false;
|
|
||||||
ConcreteFoo();
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
ctor_called,
|
|
||||||
true,
|
|
||||||
"Can call constructors of abstract supertypes"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testConcreteMethodsMustImplementTheProperNumberOfArguments()
|
|
||||||
{
|
|
||||||
assert['throws']( function()
|
|
||||||
{
|
|
||||||
AbstractFoo.extend(
|
|
||||||
{
|
|
||||||
// incorrect number of arguments
|
|
||||||
method: function()
|
|
||||||
{
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, Error, "Concrete methods must implement the proper number of argments" );
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testAbstractMethodsOfSubtypesMustImplementProperNumberOfArguments()
|
|
||||||
{
|
|
||||||
assert['throws'](
|
|
||||||
function()
|
|
||||||
{
|
|
||||||
AbstractFoo.extend(
|
|
||||||
{
|
|
||||||
// incorrect number of arguments
|
|
||||||
'abstract method': [],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
TypeError,
|
|
||||||
"Abstract methods of subtypes must implement the proper number of " +
|
|
||||||
"argments"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testAbstractMembersMayImplementMoreArgumentsThanSupertype()
|
|
||||||
{
|
|
||||||
assert.doesNotThrow(
|
|
||||||
function()
|
|
||||||
{
|
|
||||||
AbstractClass.extend( AbstractFoo,
|
|
||||||
{
|
|
||||||
// incorrect number of arguments
|
|
||||||
'abstract method': [ 'one', 'two', 'three', 'four' ],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Error,
|
|
||||||
"Abstract methods of subtypes may implement additional arguments, " +
|
|
||||||
"so long as they implement at least the required number of " +
|
|
||||||
"arguments as defined by it supertype"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testConcreteMethodsHaveNoArgumentRequirementsIfNoDefinitionGiven()
|
|
||||||
{
|
|
||||||
assert.doesNotThrow(
|
|
||||||
function()
|
|
||||||
{
|
|
||||||
AbstractClass.extend( AbstractFoo,
|
|
||||||
{
|
|
||||||
second: function( foo )
|
|
||||||
{
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
Error,
|
|
||||||
"Concrete methods needn't implement the proper number of arguments " +
|
|
||||||
"if no definition was provided"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
( function testAbstractMethodsMustBeDeclaredAsArrays()
|
|
||||||
{
|
|
||||||
assert['throws']( function()
|
|
||||||
{
|
|
||||||
Class.extend(
|
|
||||||
{
|
|
||||||
// not an array (invalid)
|
|
||||||
'abstract foo': 'scalar',
|
|
||||||
} );
|
|
||||||
}, TypeError, "Abstract methods must be declared as arrays" );
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There was an issue where the object holding the abstract methods list was not
|
|
||||||
* checking for methods by using hasOwnProperty(). Therefore, if a method such
|
|
||||||
* as toString() was defined, it would be matched in the abstract methods list.
|
|
||||||
* As such, the abstract methods count would be decreased, even though it was
|
|
||||||
* not an abstract method to begin with (nor was it removed from the list,
|
|
||||||
* because it was never defined in the first place outside of the prototype).
|
|
||||||
*
|
|
||||||
* This negative number !== 0, which causes a problem when checking to ensure
|
|
||||||
* that there are 0 abstract methods. We check explicitly for 0 for two reasons:
|
|
||||||
* (a) it's faster than <, and (b - most importantly) if it's non-zero, then
|
|
||||||
* it's either abstract or something is wrong. Negative is especially wrong. It
|
|
||||||
* should never be negative!
|
|
||||||
*/
|
|
||||||
( function testDoesNotRecognizeObjectPrototypeMembersAsAbstractWhenDefining()
|
|
||||||
{
|
|
||||||
assert.doesNotThrow( function()
|
|
||||||
{
|
|
||||||
Class.extend( SubAbstractFoo,
|
|
||||||
{
|
|
||||||
// concrete, so the result would otherwise not be abstract (return
|
|
||||||
// args so they're not optimized away during compile)
|
|
||||||
'method': function( _, __, ___ ) { return [ _, __, ___ ]; },
|
|
||||||
|
|
||||||
// the problem
|
|
||||||
'toString': function() {},
|
|
||||||
})();
|
|
||||||
}, Error, "Should not throw error if overriding a prototype method" );
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure we support named abstract class extending
|
|
||||||
*/
|
|
||||||
( function testCanCreateNamedAbstractSubtypes()
|
|
||||||
{
|
|
||||||
assert.doesNotThrow( function()
|
|
||||||
{
|
|
||||||
var cls = AbstractClass( 'NamedSubFoo' ).extend( AbstractFoo, {} );
|
|
||||||
}, Error, "Can create named abstract subtypes" );
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract classes, when extended, should yield a concrete class by default.
|
|
||||||
* Otherwise, the user should once again use AbstractClass to clearly state that
|
|
||||||
* the subtype is abstract.
|
|
||||||
*/
|
|
||||||
( function testExtendingAbstractClassIsNotAbstractByDefault()
|
|
||||||
{
|
|
||||||
var cls_named = AbstractClass( 'NamedSubFoo' ).extend( AbstractFoo, {} ),
|
|
||||||
anon_named = AbstractClass.extend( AbstractFoo, {} );
|
|
||||||
|
|
||||||
// named
|
|
||||||
assert['throws'](
|
|
||||||
function()
|
|
||||||
{
|
|
||||||
// should throw an error, since we're not declaring it as abstract
|
|
||||||
// and we're not providing a concrete impl
|
|
||||||
Class.isAbstract( cls_named.extend( {} ) );
|
|
||||||
},
|
|
||||||
TypeError,
|
|
||||||
"Extending named abstract classes should be concrete by default"
|
|
||||||
);
|
|
||||||
|
|
||||||
// anonymous
|
|
||||||
assert['throws'](
|
|
||||||
function()
|
|
||||||
{
|
|
||||||
// should throw an error, since we're not declaring it as abstract
|
|
||||||
// and we're not providing a concrete impl
|
|
||||||
Class.isAbstract( AbstractFoo.extend( {} ) );
|
|
||||||
},
|
|
||||||
TypeError,
|
|
||||||
"Extending anonymous abstract classes should be concrete by default"
|
|
||||||
);
|
|
||||||
} )();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extending an abstract class after an implement() should still result in an
|
|
||||||
* abstract class. Essentially, we are testing to ensure that the extend()
|
|
||||||
* method is properly wrapped to flag the resulting class as abstract. This was
|
|
||||||
* a bug.
|
|
||||||
*/
|
|
||||||
( function testImplementingInterfacesWillPreserveAbstractClassDeclaration()
|
|
||||||
{
|
|
||||||
// if not considered abstract, extend() will fail, as it will contain
|
|
||||||
// abstract member foo
|
|
||||||
AbstractClass( 'TestImplExtend' )
|
|
||||||
.implement( Interface( { foo: [] } ) )
|
|
||||||
.extend( {} );
|
|
||||||
} )()
|
|
||||||
|
|
Loading…
Reference in New Issue