/** * Tests overriding virtual class methods using mixins * * 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 . * * These tests vary from those in VirtualTest in that, rather than a class * overriding a virtual method defined within a trait, a trait is overriding * a method in the class that it is mixed into. In particular, since * overrides require that the super method actually exist, this means that a * trait must implement or extend a common interface. * * It is this very important (and powerful) system that allows traits to be * used as stackable modifications, similar to how one would use the * decorator pattern (but more tightly coupled). */ require( 'common' ).testCase( { caseSetUp: function() { this.Sut = this.require( 'Trait' ); this.Class = this.require( 'class' ); this.AbstractClass = this.require( 'class_abstract' ); this.Interface = this.require( 'interface' ); }, /** * A trait may implement an interface I for a couple of reasons: to have * the class mixed into be considered to of type I and to override * methods. But, regardless of the reason, let's start with the * fundamentals. */ 'Traits may implement an interface': function() { var _self = this; // simply make sure that the API is supported; nothing more. this.assertDoesNotThrow( function() { _self.Sut.implement( _self.Interface( {} ) ).extend( {} ); } ); }, /** * We would expect that the default behavior of implementing an * interface I into a trait would create a trait with all abstract * methods defined by I. */ 'Traits implementing interfaces define abstract methods': function() { var I = this.Interface( { foo: [], bar: [] } ), T = this.Sut.implement( I ).extend( {} ); var Class = this.Class, AbstractClass = this.AbstractClass; // T should contain both foo and bar as abstract methods, which we // will test indirectly in the assertions below // should fail because of abstract foo and bar this.assertThrows( function() { Class.use( T ).extend( {} ); } ); // should succeed, since we can have abstract methods within an // abstract class this.assertDoesNotThrow( function() { AbstractClass.use( T ).extend( {} ); } ); // one remaining abstract method this.assertDoesNotThrow( function() { AbstractClass.use( T ).extend( { foo: function() {} } ); } ); // both concrete this.assertDoesNotThrow( function() { Class.use( T ).extend( { foo: function() {}, bar: function() {}, } ); } ); }, /** * Just as classes implementing interfaces may choose to immediately * provide concrete definitions for the methods declared in the * interface (instead of becoming an abstract class), so too may traits. */ 'Traits may provide concrete methods for interfaces': function() { var called = false; var I = this.Interface( { foo: [] } ), T = this.Sut.implement( I ).extend( { foo: function() { called = true; }, } ); var Class = this.Class; this.assertDoesNotThrow( function() { // should invoke concrete foo; class definition should not fail, // because foo is no longer abstract Class.use( T ).extend( {} )().foo(); } ); this.assertOk( called ); }, /** * Instances of class C mixing in some trait T implementing I will be * considered to be of type I, since any method of I would either be * defined within T, or would be implicitly abstract in T, requiring its * definition within C; otherwise, C would have to be declared astract. */ 'Instance of class mixing in trait implementing I is of type I': function() { var I = this.Interface( {} ), T = this.Sut.implement( I ).extend( {} ); this.assertOk( this.Class.isA( I, this.Class.use( T ).extend( {} )() ) ); }, /** * The API for multiple interfaces should be the same for traits as it * is for classes. */ 'Trait can implement multiple interfaces': function() { var Ia = this.Interface( {} ), Ib = this.Interface( {} ), T = this.Sut.implement( Ia, Ib ).extend( {} ), o = this.Class.use( T ).extend( {} )(); this.assertOk( this.Class.isA( Ia, o ) ); this.assertOk( this.Class.isA( Ib, o ) ); }, /** * This is a concept borrowed from Scala: consider class C and trait T, * both implementing interface I which declares method M. T should be * able to override C.M so long as it is concrete, but to do so, we need * some way of telling ease.js that we are overriding at time of mixin; * otherwise, override does not make sense, because I.M is clearly * abstract and there is nothing to override. */ 'Mixin can override virtual concrete method defined by interface': function() { var called = false, I = this.Interface( { foo: [] } ); var T = this.Sut.implement( I ).extend( { // the keyword combination `abstract override' indicates that we // should override whatever concrete implementation was defined // before our having been mixed in 'abstract override foo': function() { called = true; }, } ); var _self = this; var C = this.Class.implement( I ).extend( { // this should be overridden by the mixin and should therefore // never be called (for __super tests, see LinearizationTest) 'virtual foo': function() { _self.fail( false, true, "Concrete class method was not overridden by mixin" ); }, } ); // mixing in a trait atop of C should yield the results described // above due to the `abstract override' keyword combination C.use( T )().foo(); this.assertOk( called ); }, } );