diff --git a/test/Class/ExtendTest.js b/test/Class/ExtendTest.js new file mode 100644 index 0000000..53a4951 --- /dev/null +++ b/test/Class/ExtendTest.js @@ -0,0 +1,431 @@ +/** + * Tests class module extend() method + * + * Copyright (C) 2010, 2011, 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 . + * + * Note that these tests all use the `new' keyword for instantiating + * classes, even though it is not required with ease.js; this is both for + * historical reasons (when `new' was required during early development) and + * because we are not testing (and do want to depend upon) that feature. + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.test_props = { + one: 1, + two: 2, + }; + + this.Sut = this.require( 'class' ); + + // there are two different means of extending; we want to test them + // both (this will be denoted Foo) + this.classes = [ + this.Sut.extend( this.test_props ), + this.Sut( this.test_props ), + ]; + }, + + + /** + * All classes can be easily extended via an extend method, although it + * is not necessarily recommended to be used directly, as you must + * ensure that the object is an ease.js class and the resulting class + * will be anonymous. + */ + '@each(classes) Created class contains extend method': function( C ) + { + this.assertOk( typeof C.extend === 'function' ); + }, + + + /** + * It would make sense that a subtype returned is an object, since it + * cannot be a class if it isn't. + */ + '@each(classes) Subtype is returned as an object': function( C ) + { + this.assertOk( C.extend() instanceof Object ); + }, + + + /** + * Subtypes should inherit all properties of the supertype into their + * prototype chain. + */ + '@each(classes) Subtype inherits parent properties': function( C ) + { + var SubFoo = C.extend(); + + for ( var prop in this.test_props ) + { + this.assertEqual( + this.test_props[ prop ], + SubFoo.prototype[ prop ], + "Missing property: " + prop + ); + } + }, + + + /** + * A subtype should obvious contain the properties that were a part of + * its definition. + */ + '@each(classes) Subtype contains its own properties': function( C ) + { + var sub_props = { + three: 3, + four: 4, + }; + + var sub_foo = new C.extend( sub_props )(); + + // and ensure that the subtype's properties were included + for ( var prop in sub_props ) + { + this.assertEqual( + sub_props[ prop ], + sub_foo[ prop ], + "Missing property: " + prop + ); + } + }, + + + /** + * In addition to the core functions provided by ease.js for checking + * instances, we try to ease into the protype model the best we can in + * order to work with other prototypes; therefore, instances should be + * recognized as instances of their parent classes even by the + * ECMAScript `instanceof' operator. + */ + '@each(classes) Subtypes are ECMAScript instances of their supertypes': + function( C ) + { + this.assertOk( C.extend()() instanceof C ); + }, + + + /** + * Even though this can be checked using the instanceof operator, + * ease.js has a more complex type system (e.g. supporting of + * interfaces) and so we want to provide a consistent alternative. + */ + '@each(classes) Subtypes are easejs instances of their supertypes': + function( C ) + { + var SubFoo = C.extend(), + sub_instance = new SubFoo(); + + this.assertOk( sub_instance.isInstanceOf( SubFoo ) ); + }, + + + /* + * Foo + * | + * SubFoo + * / \ + * SubSubFoo SubSubFoo2 + * + / + + /** + * Objects should be considered instances of any classes that their + * instantiating class inherits from, since they inherit their API and + * are interchangable, provided that only the common subset of the API + * is used. + */ + '@each(classes) Objects are instances of their super-supertypes': + function( C ) + { + var sub_sub_instance = new ( C.extend().extend() )(); + + this.assertOk( + ( ( sub_sub_instance instanceof C ) + && sub_sub_instance.isInstanceOf( C ) + ) + ); + }, + + + /** + * It would not make sense that an object is considered to be an + * instance of any possible subtypes---that is, if C inherits B, then an + * instance of B is not of type C; C could introduce an incompatible + * interface. + */ + '@each(classes) Objects are not instances of subtypes': function( C ) + { + var SubFoo = C.extend(), + SubSubFoo = SubFoo.extend(), + sub_inst = new SubFoo(); + + this.assertOk( + ( !( sub_inst instanceof SubSubFoo ) + && !( sub_inst.isInstanceOf( SubSubFoo ) ) + ) + ); + }, + + + /** + * Two classes that inherit from a common parent are not compatible, as + * they can introduce their own distinct interfaces. + */ + '@each(classes) Objects are not instances of sibling types': + function( C ) + { + var SubFoo = C.extend(), + SubSubFoo = SubFoo.extend(), + SubSubFoo2 = SubFoo.extend(), + + sub_sub2_inst = new SubSubFoo2(); + + this.assertOk( + ( !( sub_sub2_inst instanceof SubSubFoo ) + && !( sub_sub2_inst.isInstanceOf( SubSubFoo ) ) + ) + ); + }, + + + /** + * We support extending existing prototypes (that is, inherit from + * constructors that were not created using ease.js). + */ + 'Constructor prototype is copied to subclass': function() + { + var Ctor = function() {}; + Ctor.prototype = { foo: {} }; + + this.assertStrictEqual( + this.Sut.extend( Ctor, {} ).prototype.foo, + Ctor.prototype.foo + ); + }, + + + /** + * This should go without saying---we're aiming for consistency here and + * subclassing doesn't make much sense if it doesn't work. + */ + 'Subtype of constructor should contain extended members': function() + { + var Ctor = function() {}; + + this.assertNotEqual( + ( new this.Sut.extend( Ctor, { foo: {} } )() ).foo, + undefined + ); + }, + + + /** + * If a subtype provides a property of the same name as its parent, then + * it should act as a reassignment. + */ + 'Subtypes can override parent property values': function() + { + var expect = 'ok', + C = this.Sut.extend( { p: null } ).extend( { p: expect } ); + + this.assertEqual( C().p, expect ); + }, + + + /** + * Prevent overriding the internal method that initializes property + * values upon instantiation. + */ + '__initProps() cannot be declared (internal method)': function() + { + var _self = this; + + this.assertThrows( function() + { + _self.Sut.extend( + { + __initProps: function() {}, + } ); + }, Error ); + }, + + + // TODO: move me into a more appropriate test case (this may actually be + // tested elsewhere) + /** + * If using the short-hand extend, an object is required to represent + * the class defintiion. + */ + 'Invoking class module requires object as argument if extending': + function() + { + var _self = this; + + // these tests can be run in the browser in pre-ES5 environments, so + // no forEach() + var chk = [ 5, false, undefined ], + i = chk.length; + + while ( i-- ) + { + this.assertThrows( function() + { + _self.Sut( chk[ i ] ); + }, + TypeError + ); + } + }, + + + /** + * We provide a useful default toString() method, but one may wish to + * override it + */ + 'Can override toString() method': function() + { + var str = 'foomookittypoo', + result = '' + ; + + result = this.Sut( 'FooToStr', + { + toString: function() + { + return str; + }, + } )().toString(); + + this.assertEqual( result, str ); + }, + + + /** + * In ease.js's initial design, keywords were not included. This meant + * that duplicate member definitions were not possible---it'd throw a + * parse error (maybe). However, with keywords, it is now possible to + * redeclare a member with the same name in the same class definition. + * Since this doesn't make much sense, we must disallow it. + */ + 'Cannot provide duplicate member definitions using unique keys': + function() + { + var _self = this; + + this.assertThrows( function() + { + _self.Sut( + { + // declare as protected first so that we won't get a visibility + // de-escalation error with the below re-definition + 'protected foo': '', + + // should fail; redefinition + 'public foo': '', + } ); + }, Error ); + + this.assertThrows( function() + { + _self.Sut( + { + // declare as protected first so that we won't get a visibility + // de-escalation error with the below re-definition + 'protected foo': function() {}, + + // should fail; redefinition + 'public foo': function() {}, + } ); + }, Error ); + }, + + + /** + * To understand this test, one must understand how "inheritance" works + * with prototypes. We must create a new instance of the ctor (class) + * and add that instance to the prototype chain (if we added an + * un-instantiated constructor, then the members in the prototype would + * be accessible only though ctor.prototype). Therefore, when we + * instantiate this class for use in the prototype, we must ensure the + * constructor is not invoked, since our intent is not to create a new + * instance of the class. + */ + '__construct should not be called when extending class': function() + { + var called = false, + Foo = this.Sut( { + 'public __construct': function() + { + called = true; + } + } ).extend( {} ); + + this.assertEqual( called, false ); + }, + + + /** + * Previously, when attempting to extend from an invalid supertype, + * you'd get a CALL_NON_FUNCTION_AS_CONSTRUCTOR error, which is not very + * helpful to someone who is not familiar with the ease.js internals. + * Let's provide a more useful error that clearly states what's going + * on. + */ + 'Extending from non-ctor or non-class provides useful error': function() + { + try + { + // invalid supertype + this.Sut.extend( 'oops', {} ); + } + catch ( e ) + { + this.assertOk( e.message.search( 'extend from' ), + "Error message for extending from non-ctor or class " + + "makes sense" + ); + + return; + } + + this.assertFail( + "Attempting to extend from non-ctor or class should " + + "throw exception" + ); + }, + + + /** + * If we attempt to extend an object (rather than a constructor), we + * should simply use that as the prototype directly rather than + * attempting to instantiate it. + */ + 'Extending object will not attempt instantiation': function() + { + var obj = { foo: 'bar' }; + + this.assertEqual( obj.foo, this.Sut.extend( obj, {} )().foo, + "Should be able to use object as prototype" + ); + }, +} ); diff --git a/test/Class/InstanceSafetyTest.js b/test/Class/InstanceSafetyTest.js new file mode 100644 index 0000000..46952dc --- /dev/null +++ b/test/Class/InstanceSafetyTest.js @@ -0,0 +1,87 @@ +/** + * Tests safety of class instances + * + * Copyright (C) 2010, 2011, 2013, 2014 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 . + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'class' ); + }, + + + /** + * Ensure that we're not getting/setting values of the prototype, which + * would have disasterous implications (=== can also be used to test for + * references, but this test demonstrates the functionality that we're + * looking to ensure) + */ + 'Multiple instances of same class do not share array references': + function() + { + var C = this.Sut.extend( { arr: [] } ), + obj1 = new C(), + obj2 = new C(); + + obj1.arr.push( 'one' ); + obj2.arr.push( 'two' ); + + // if the arrays are distinct, then each will have only one element + this.assertEqual( obj1.arr[ 0 ], 'one' ); + this.assertEqual( obj2.arr[ 0 ], 'two' ); + this.assertEqual( obj1.arr.length, 1 ); + this.assertEqual( obj2.arr.length, 1 ); + }, + + + /** + * Same concept as above, but with objects instead of arrays. + */ + 'Multiple instances of same class do not share object references': + function() + { + var C = this.Sut.extend( { obj: {} } ), + obj1 = new C(), + obj2 = new C(); + + obj1.obj.a = true; + obj2.obj.b = true; + + this.assertEqual( obj1.obj.a, true ); + this.assertEqual( obj1.obj.b, undefined ); + + this.assertEqual( obj2.obj.a, undefined ); + this.assertEqual( obj2.obj.b, true ); + }, + + + /** + * Ensure that the above checks extend to subtypes. + */ + 'Instances of subtypes do not share property references': function() + { + var C2 = this.Sut.extend( { arr: [], obj: {} } ).extend( {} ), + obj1 = new C2(), + obj2 = new C2(); + + this.assertNotEqual( obj1.arr !== obj2.arr ); + this.assertNotEqual( obj1.obj !== obj2.obj ); + }, +} ); diff --git a/test/inc-testcase.js b/test/inc-testcase.js index 50e3477..b3c3892 100644 --- a/test/inc-testcase.js +++ b/test/inc-testcase.js @@ -116,6 +116,11 @@ module.exports = function( test_case ) if ( method === 'each' ) { + if ( !( context[ prop ] ) ) + { + throw Error( "Unknown @each context: " + prop ); + } + count = context[ prop ].length; args = []; diff --git a/test/test-class-extend.js b/test/test-class-extend.js deleted file mode 100644 index 6e01192..0000000 --- a/test/test-class-extend.js +++ /dev/null @@ -1,463 +0,0 @@ -/** - * Tests class module extend() method - * - * Copyright (C) 2010, 2011, 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 . - */ - -var common = require( './common' ), - assert = require( 'assert' ), - Class = common.require( 'class' ); - -var foo_props = { - one: 1, - two: 2, - }, - - // there are two different means of extending; we want to test them both - classes = [ - Class.extend( foo_props ), - Class( foo_props ), - ], - - class_count = classes.length, - - // will hold the class being tested - Foo = null -; - - -// Run all tests for both. This will ensure that, regardless of how the class is -// created, it operates as it should. Fortunately, these tests are fairly quick. -for ( var i = 0; i < class_count; i++ ) -{ - Foo = classes[ i ]; - - assert.ok( - ( Foo.extend instanceof Function ), - "Created class contains extend method" - ); - - var sub_props = { - three: 3, - four: 4, - }, - - SubFoo = Foo.extend( sub_props ), - sub_foo = SubFoo() - ; - - assert.ok( - ( SubFoo instanceof Object ), - "Subtype is returned as an object" - ); - - // ensure properties were inherited from supertype - for ( var prop in foo_props ) - { - assert.equal( - foo_props[ prop ], - SubFoo.prototype[ prop ], - "Subtype inherits parent properties: " + prop - ); - } - - // and ensure that the subtype's properties were included - for ( var prop in sub_props ) - { - assert.equal( - sub_props[ prop ], - sub_foo[ prop ], - "Subtype contains its own properties: " + prop - ); - } - - - var sub_instance = new SubFoo(); - - assert.ok( - ( sub_instance instanceof Foo ), - "Subtypes are considered to be instances of their supertypes " + - "(via instanceof operator)" - ); - - assert.ok( - sub_instance.isInstanceOf( SubFoo ), - "Subtypes are considered to be instances of their supertypes (via " + - "isInstanceOf method)" - ); - - - // Foo - // | - // SubFoo - // / \ - // SubSubFoo SubSubFoo2 - // - var SubSubFoo = SubFoo.extend(), - SubSubFoo2 = SubFoo.extend(), - - sub_sub_instance = new SubSubFoo(), - sub_sub2_instance = new SubSubFoo2(); - - assert.ok( - ( ( sub_sub_instance instanceof Foo ) - && sub_sub_instance.isInstanceOf( Foo ) - ), - "Sub-subtypes should be instances of their super-supertype" - ); - - assert.ok( - ( !( sub_instance instanceof SubSubFoo ) - && !( sub_instance.isInstanceOf( SubSubFoo ) ) - ), - "Supertypes should not be considered instances of their subtypes" - ); - - assert.ok( - ( !( sub_sub2_instance instanceof SubSubFoo ) - && !( sub_sub2_instance.isInstanceOf( SubSubFoo ) ) - ), - "Subtypes should not be considered instances of their siblings" - ); - - - // to test inheritance of classes that were not previously created via the - // Class.extend() method - var OtherClass = function() {}; - OtherClass.prototype = - { - foo: 'bla', - }; - - var SubOther = Class.extend( OtherClass, - { - newFoo: 2, - }); - - - assert.equal( - SubOther.prototype.foo, - OtherClass.prototype.foo, - "Prototype of existing class should be copied to subclass" - ); - - assert.notEqual( - SubOther().newFoo, - undefined, - "Subtype should contain extended members" - ); - - - assert['throws']( function() - { - Class.extend( OtherClass, - { - foo: function() {}, - }); - }, TypeError, "Cannot override property with a method" ); - - - var AnotherFoo = Class.extend( - { - arr: [], - obj: {}, - }); - - var Obj1 = new AnotherFoo(), - Obj2 = new AnotherFoo(); - - Obj1.arr.push( 'one' ); - Obj2.arr.push( 'two' ); - - Obj1.obj.a = true; - Obj2.obj.b = true; - - // to ensure we're not getting/setting values of the prototype (=== can also be - // used to test for references, but this test demonstrates the functionality - // that we're looking to ensure) - assert.ok( - ( ( Obj1.arr[ 0 ] === 'one' ) && ( Obj2.arr[ 0 ] === 'two' ) ), - "Multiple instances of the same class do not share array references" - ); - - assert.ok( - ( ( ( Obj1.obj.a === true ) && ( Obj1.obj.b === undefined ) ) - && ( ( Obj2.obj.a === undefined ) && ( Obj2.obj.b === true ) ) - ), - "Multiple instances of the same class do not share object references" - ); - - var arr_val = 1; - var SubAnotherFoo = AnotherFoo.extend( - { - arr: [ arr_val ], - }); - - var SubObj1 = new SubAnotherFoo(), - SubObj2 = new SubAnotherFoo(); - - assert.ok( - ( ( SubObj1.arr !== SubObj2.arr ) && ( SubObj1.obj !== SubObj2.obj ) ), - "Instances of subtypes do not share property references" - ); - - assert.ok( - ( ( SubObj1.arr[ 0 ] === arr_val ) && ( SubObj2.arr[ 0 ] === arr_val ) ), - "Subtypes can override parent property values" - ); - - assert['throws']( function() - { - Class.extend( - { - __initProps: function() {}, - }); - }, Error, "__initProps() cannot be declared (internal method)" ); - - - var SubSubAnotherFoo = AnotherFoo.extend(), - SubSubObj1 = new SubSubAnotherFoo(), - SubSubObj2 = new SubSubAnotherFoo(); - - // to ensure the effect is recursive - assert.ok( - ( ( SubSubObj1.arr !== SubSubObj2.arr ) - && ( SubSubObj1.obj !== SubSubObj2.obj ) - ), - "Instances of subtypes do not share property references" - ); -} - - -( function testInvokingClassModuleRequiresObjectAsArgumentIfCreating() -{ - assert['throws']( function() - { - Class( 'moo' ); - Class( 5 ); - Class( false ); - Class(); - }, - TypeError, - "Invoking class module requires object as argument if extending " + - "from base class" - ); - - var args = [ {}, 'one', 'two', 'three' ]; - - // we must only provide one argument if the first argument is an object (the - // class definition) - try - { - Class.apply( null, args ); - - // if all goes well, we don't get to this line - assert.fail( - "Only one argument for class definitions is permitted" - ); - } - catch ( e ) - { - assert.notEqual( - e.message.match( args.length + ' given' ), - null, - "Class invocation should give argument count on error" - ); - } -} )(); - - -/** - * We provide a useful default toString() method, but one may wish to override - * it - */ -( function testCanOverrideToStringMethod() -{ - var str = 'foomookittypoo', - result = '' - ; - - result = Class( 'FooToStr', - { - toString: function() - { - return str; - }, - bla: function() {}, - })().toString(); - - assert.equal( - result, - str, - "Can override default toString() method of class" - ); -} )(); - - -/** - * In ease.js's initial design, keywords were not included. This meant that - * duplicate member definitions were not possible - it'd throw a parse error. - * However, with keywords, it is now possible to redeclare a member with the - * same name in the same class definition. Since this doesn't make much sense, - * we must disallow it. - */ -( function testCannotProvideDuplicateMemberDefintions() -{ - assert['throws']( function() - { - Class( - { - // declare as protected first so that we won't get a visibility - // de-escalation error with the below re-definition - 'protected foo': '', - - // should fail; redefinition - 'public foo': '', - } ); - }, Error, "Cannot redeclare property in same class definition" ); - - assert['throws']( function() - { - Class( - { - // declare as protected first so that we won't get a visibility - // de-escalation error with the below re-definition - 'protected foo': function() {}, - - // should fail; redefinition - 'public foo': function() {}, - } ); - }, Error, "Cannot redeclare method in same class definition" ); -} )(); - - -/** - * To understand this test, one must understand how "inheritance" works - * with prototypes. We must create a new instance of the ctor (class) and add - * that instance to the prototype chain (if we added an un-instantiated - * constructor, then the members in the prototype would be accessible only - * though ctor.prototype). Therefore, when we instantiate this class for use in - * the prototype, we must ensure the constructor is not invoked, since our - * intent is not to create a new instance of the class. - */ -( function testConstructorShouldNotBeCalledWhenExtendingClass() -{ - var called = false, - Foo = Class( { - 'public __construct': function() - { - called = true; - } - } ).extend( {} ); - - assert.equal( called, false, - "Constructor should not be called when extending a class" - ); -} )(); - - -/** - * Previously, when attempting to extend from an invalid supertype, you'd get a - * CALL_NON_FUNCTION_AS_CONSTRUCTOR error, which is not very helpful to someone - * who is not familiar with the ease.js internals. Let's provide a more useful - * error that clearly states what's going on. - */ -( function testExtendingFromNonCtorOrClassProvidesUsefulError() -{ - try - { - // invalid supertype - Class.extend( 'oops', {} ); - } - catch ( e ) - { - assert.ok( e.message.search( 'extend from' ), - "Error message for extending from non-ctor or class makes sense" - ); - - return; - } - - assert.fail( - "Attempting to extend from non-ctor or class should throw exception" - ); -} )(); - - -/** - * Only virtual methods may be overridden. - */ -( function testCannotOverrideNonVirtualMethod() -{ - try - { - var Foo = Class( - { - // non-virtual - 'public foo': function() {}, - } ), - - SubFoo = Foo.extend( - { - // should fail (cannot override non-virtual method) - 'override public foo': function() {}, - } ); - } - catch ( e ) - { - assert.ok( e.message.search( 'foo' ), - "Non-virtual override error message should contain name of method" - ); - - return; - } - - assert.fail( "Should not be permitted to override non-virtual method" ); -} )(); - - -/** - * If we attempt to extend an object (rather than a constructor), we should - * simply use that as the prototype directly rather than attempting to - * instantiate it. - */ -( function testExtendingObjectWillNotAttemptInstantiation() -{ - var obj = { foo: 'bar' }; - - assert.equal( obj.foo, Class.extend( obj, {} )().foo, - 'Should be able to use object as prototype' - ); -} )(); - - -/** - * It only makes sense to extend from an object or function (constructor, more - * specifically) - * - * We could also test to ensure that the return value of the constructor is an - * object, but that is unnecessary for the time being. - */ -( function testWillThrowExceptionIfNonObjectOrCtorIsProvided() -{ - assert['throws']( function() - { - Class.extend( 'foo', {} ); - }, TypeError, 'Should not be able to extend from non-object or non-ctor' ); -} )(); -