/** * Tests basic trait definition * * Copyright (C) 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( 'Trait' ); this.Class = this.require( 'class' ); // means of creating anonymous traits this.ctor = [ this.Sut.extend, this.Sut, ]; // trait field name conflicts (methods) this.fconflict = [ [ 'foo', "same name; no keywords", { foo: function() {} }, { foo: function() {} }, ], [ 'foo', "same keywords; same visibility", { 'public foo': function() {} }, { 'public foo': function() {} }, ], // should (at least for the time being) be picked up by existing // class error checks; TODO: but let's provide trait-specific // error messages to avoid frustration and infuriation [ 'foo', "varying keywords; same visibility", { 'virtual public foo': function() {} }, { 'public virtual foo': function() {} }, ], [ 'foo', "different visibility", { 'public foo': function() {} }, { 'protected foo': function() {} }, ], ]; }, /** * We continue with the same concept used for class * definitions---extending the Trait module itself will create an * anonymous trait. */ '@each(ctor) Can extend Trait to create anonymous trait': function( T ) { this.assertOk( this.Sut.isTrait( T( {} ) ) ); }, /** * A trait can only be used by something else---it does not make sense * to instantiate them directly, since they form an incomplete picture. */ '@each(ctor) Cannot instantiate trait without error': function( T ) { this.assertThrows( function() { T( {} )(); }, Error ); }, /** * One way that traits acquire meaning is by their use in creating * classes. This also allows us to observe whether traits are actually * working as intended without testing too closely to their * implementation. This test simply ensures that the Class module will * accept our traits. * * Classes consume traits as part of their definition using the `use' * method. We should be able to then invoke the `extend' method to * provide our own definition, without having to inherit from another * class. */ '@each(ctor) Base class definition is applied when using traits': function( T ) { var expected = 'bar'; var C = this.Class.use( T( {} ) ).extend( { foo: expected, } ); this.assertOk( this.Class.isClass( C ) ); this.assertEqual( C().foo, expected ); }, /** * Traits contribute to the definition of the class that `use's them; * therefore, it would stand to reason that we should still be able to * inherit from a supertype while using traits. */ '@each(ctor) Supertype definition is applied when using traits': function( T ) { var expected = 'bar'; expected2 = 'baz'; Foo = this.Class( { foo: expected } ), SubFoo = this.Class.use( T( {} ) ) .extend( Foo, { bar: expected2 } ); var inst = SubFoo(); this.assertOk( this.Class.isA( Foo, inst ) ); this.assertEqual( inst.foo, expected, "Supertype failure" ); this.assertEqual( inst.bar, expected2, "Subtype failure" ); }, /** * The above tests have ensured that classes are still operable with * traits; we can now test that traits are mixed into the class * definition via `use' by asserting on the trait definitions. */ '@each(ctor) Trait definition is mixed into base class definition': function( T ) { var called = false; var Trait = T( { foo: function() { called = true; } } ), inst = this.Class.use( Trait ).extend( {} )(); // if mixin was successful, then we should have the `foo' method. this.assertDoesNotThrow( function() { inst.foo(); }, Error, "Should have access to mixed in fields" ); // if our variable was not set, then it was a bs copy this.assertOk( called, "Mixed in field copy error" ); }, /** * The above test should apply just the same to subtypes. */ '@each(ctor) Trait definition is mixed into subtype definition': function( T ) { var called = false; var Trait = T( { foo: function() { called = true; } } ), Foo = this.Class( {} ), inst = this.Class.use( Trait ).extend( Foo, {} )(); inst.foo(); this.assertOk( called ); }, // // At this point, we assume that each ctor method is working as expected // (that is---the same); we will proceed to test only a single method of // construction under that assumption. // /** * Traits cannot be instantiated, so they need not define __construct * for themselves; however, they may wish to influence the construction * of anything that uses them. This is poor practice, since that * introduces a war between traits to take over the constructor; * instead, the class using the traits should handle calling the methods * on the traits and we should disallow traits from attempting to set * the constructor. */ 'Traits cannot define __construct': function() { try { this.Sut( { __construct: function() {} } ); } catch ( e ) { this.assertOk( e.message.match( /\b__construct\b/ ) ); return; } this.fail( "Traits should not be able to define __construct" ); }, /** * If two traits attempt to define the same field (by name, regardless * of its type), then an error should be thrown to warn the developer of * a problem; automatic resolution would be a fertile source of nasty * and confusing bugs. * * TODO: conflict resolution through aliasing */ '@each(fconflict) Cannot mix in multiple concrete methods of same name': function( dfns ) { var fname = dfns[ 0 ], desc = dfns[ 1 ], A = this.Sut( dfns[ 2 ] ), B = this.Sut( dfns[ 3 ] ); // this, therefore, should error try { this.Class.use( A, B ).extend( {} ); } catch ( e ) { // the assertion should contain the name of the field that // caused the error this.assertOk( e.message.match( '\\b' + fname + '\\b' ), "Error message missing field name: " + e.message ); // TODO: we can also make less people hate us if we include the // names of the conflicting traits; in the case of an anonymous // trait, maybe include its index in the use list return; } this.fail( false, true, "Mixin must fail on conflict: " + desc ); }, /** * Traits in ease.js were designed in such a way that an object can be * considered to be a type of any of the traits that its class mixes in; * this is consistent with the concept of interfaces and provides a very * simple and intuitive type system. */ 'A class is considered to be a type of each used trait': function() { var Ta = this.Sut( {} ), Tb = this.Sut( {} ), Tc = this.Sut( {} ), o = this.Class.use( Ta, Tb ).extend( {} )(); // these two were mixed in this.assertOk( this.Class.isA( Ta, o ) ); this.assertOk( this.Class.isA( Tb, o ) ); // this one was not this.assertOk( this.Class.isA( Tc, o ) === false ); }, } );