1
0
Fork 0
easejs/test/MethodWrappersTest.js

384 lines
10 KiB
JavaScript
Raw Normal View History

/**
* Tests method sut
*
2011-12-23 00:09:01 -05:00
* Copyright (C) 2010,2011 Mike Gerwitz
*
* This file is part of ease.js.
*
* ease.js is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Mike Gerwitz
*/
var common = require( './common' ),
assert = require( 'assert' ),
util = common.require( 'util' ),
sut = common.require( 'MethodWrappers' )
;
/**
* The wrappers accept a function that should return the instance to be bound to
* 'this' when invoking a method. This has some important consequences, such as
* the ability to implement protected/private members.
*/
( function testMethodInvocationBindsThisToPassedInstance()
{
var instance = function() {},
val = 'fooboo',
val2 = 'fooboo2',
iid = 1,
called = false,
getInst = function()
{
called = true;
return instance;
},
method = sut.standard.wrapNew(
function()
{
return this.foo;
},
null, 0, getInst
),
override = sut.standard.wrapOverride(
function()
{
return this.foo2;
},
method, 0, getInst
)
;
// set instance values
instance.foo = val;
instance.foo2 = val2;
assert.equal( method(), val,
"Calling method will bind 'this' to passed instance"
);
assert.equal( override(), val2,
"Calling method override will bind 'this' to passed instance"
);
} )();
/**
* The __super property is defined for method overrides and permits invoking the
* overridden method (method of the supertype).
*
* In this test, we are not looking to assert that __super matches the super
* method. Rather, we want to ensure it /invokes/ it. This is because the super
* method may be wrapped to provide additional functionality. We don't know, we
* don't care. We just want to make sure it's functioning properly.
*/
( function testOverridenMethodShouldContainReferenceToSuperMethod()
{
var orig_called = false,
getInst = function() {},
// "super" method
method = sut.standard.wrapNew(
function()
{
orig_called = true;
},
null, 0, getInst
),
// override method
override = sut.standard.wrapOverride(
function()
{
assert.notEqual(
this.__super,
undefined,
"__super is defined for overridden method"
);
this.__super();
assert.equal(
orig_called,
true,
"Invoking __super calls super method"
);
},
method, 0, getInst
)
;
// invoke the method to run the above assertions
override();
} )();
/**
* If the method is called when bound to a different context (e.g. for
* protected/private members), __super may not be properly bound.
*
* This test is in response to a bug found after implementing visibility
* support. The __super() method was previously defined on 'this', which may or
* may not be the context that is actually used. Likely, it's not.
*/
( function testSuperMethodWorksProperlyWhenContextDiffers()
{
var super_called = false,
retobj = {},
getInst = function()
{
return retobj;
},
// super method to be overridden
method = sut.standard.wrapNew(
function()
{
super_called = true;
},
null, 0, getInst
),
// the overriding method
override = sut.standard.wrapOverride(
function()
{
this.__super();
},
method, 0, getInst
)
;
// call the overriding method
override();
// ensure that the super method was called
assert.equal( super_called, true,
"__super() method is called even when context differs"
);
// finally, ensure that __super is no longer set on the returned object
// after the call to ensure that the caller cannot break encapsulation by
// stealing a method reference (sneaky, sneaky)
assert.equal( retobj.__super, undefined,
"__super() method is unset after being called"
);
} )();
Added `proxy' keyword support The concept of proxy methods will become an important, core concept in ease.js that will provide strong benefits for creating decorators and proxies, removing boilerplate code and providing useful metadata to the system. Consider the following example: Class( 'Foo', { // ... 'public performOperation': function( bar ) { this._doSomethingWith( bar ); return this; }, } ); Class( 'FooDecorator', { 'private _foo': null, // ... 'public performOperation': function( bar ) { return this._foo.performOperation( bar ); }, } ); In the above example, `FooDecorator` is a decorator for `Foo`. Assume that the `getValueOf()` method is undecorated and simply needs to be proxied to its component --- an instance of `Foo`. (It is not uncommon that a decorator, proxy, or related class will alter certain functionality while leaving much of it unchanged.) In order to do so, we can use this generic, boilerplate code return this.obj.func.apply( this.obj, arguments ); which would need to be repeated again and again for *each method that needs to be proxied*. We also have another problem --- `Foo.getValueOf()` returns *itself*, which `FooDecorator` *also* returns. This breaks encapsulation, so we instead need to return ourself: 'public performOperation': function( bar ) { this._foo.performOperation( bar ); return this; }, Our boilerplate code then becomes: var ret = this.obj.func.apply( this.obj, arguments ); return ( ret === this.obj ) ? this : ret; Alternatively, we could use the `proxy' keyword: Class( 'FooDecorator2', { 'private _foo': null, // ... 'public proxy performOperation': '_foo', } ); `FooDecorator2.getValueOf()` and `FooDecorator.getValueOf()` both perform the exact same task --- proxy the entire call to another object and return its result, unless the result is the component, in which case the decorator itself is returned. Proxies, as of this commit, accomplish the following: - All arguments are forwarded to the destination - The return value is forwarded to the caller - If the destination returns a reference to itself, it will be replaced with a reference to the caller's context (`this`). - If the call is expected to fail, either because the destination is not an object or because the requested method is not a function, a useful error will be immediately thrown (rather than the potentially cryptic one that would otherwise result, requiring analysis of the stack trace). N.B. As of this commit, static proxies do not yet function properly.
2012-05-02 13:26:47 -04:00
/**
* The proxy wrapper should forward all arguments to the provided object's
* appropriate method. The return value should also be proxied back to the
* caller.
*/
( function testProxyWillProperlyForwardCallToDestinationObject()
{
var name = 'someMethod',
propname = 'dest',
args = [ 1, {}, 'three' ],
args_given = [],
getInst = function()
{
return inst;
},
method_retval = {},
dest = {
someMethod: function()
{
args_given = Array.prototype.slice.call( arguments );
return method_retval;
},
},
// acts like a class instance
inst = { dest: dest },
proxy = sut.standard.wrapProxy( propname, null, 0, getInst, name )
;
assert.strictEqual( method_retval, proxy.apply( inst, args ),
"Proxy call should return the value from the destination"
);
assert.deepEqual( args, args_given,
"All arguments should be properly forwarded to the destination"
);
} )();
/**
* If the destination object returns itself, then we should return the context
* in which the proxy was called; this ensures that we do not break
* encapsulation. Consequently, it also provides a more consistent and sensical
* API and permits method chaining.
*
* If this is not the desired result, then the user is free to forefit the proxy
* wrapper and instead use a normal method, manually proxying the call.
*/
( function testProxyReturnValueIsReplacedWithContextIfDestinationReturnsSelf()
{
var propname = 'foo',
method = 'bar',
foo = {
bar: function()
{
// return "self"
return foo;
}
},
inst = { foo: foo },
ret = sut.standard.wrapProxy(
propname, null, 0,
function()
{
return inst;
},
method
).call( inst )
;
assert.strictEqual( inst, ret,
"Proxy should return instance in place of destination, if returned"
);
} )();
// common assertions between a couple of proxy tests
function proxyErrorAssertCommon( e, prop, method )
{
assert.ok(
e.message.search( 'Unable to proxy' ) > -1,
"Unexpected error received: " + e.message
);
assert.ok(
( ( e.message.search( prop ) > -1 )
&& ( e.message.search( method ) > -1 )
),
"Error should contain property and method names"
);
}
/**
* Rather than allowing a cryptic error to be thrown by the engine, take some
* initiative and attempt to detect when a call will fail due to the destination
* not being an object.
*/
( function testProxyThrowsErrorIfCallWillFailDueToNonObject()
{
var prop = 'noexist',
method = 'foo';
try
{
// should fail because 'noexist' does not exist on the object
sut.standard.wrapProxy(
prop, null, 0,
function() { return {}; },
method
)();
}
catch ( e )
{
proxyErrorAssertCommon( e, prop, method );
return;
}
assert.fail(
"Error should be thrown if proxy would fail due to a non-object"
);
} )();
/**
* Rather than allowing a cryptic error to be thrown by the engine, take some
* initiative and attempt to detect when a call will fail due to the destination
* method not being a function.
*/
( function testProxyThrowsErrorIfCallWillFailDueToNonObject()
{
var prop = 'dest',
method = 'foo';
try
{
// should fail because 'noexist' does not exist on the object
sut.standard.wrapProxy(
prop, null, 0,
function() { return { dest: { foo: 'notafunc' } }; },
method
)();
}
catch ( e )
{
proxyErrorAssertCommon( e, prop, method );
return;
}
assert.fail(
"Error should be thrown if proxy would fail due to a non-function"
);
} )();
/**
* If the `static' keyword is provided, then the proxy mustn't operate on
* instance properties. Instead, the static accessor method $() must be used.
*/
( function testCanProxyToStaticMembers()
{
var getInst = function()
{
// pretend that we're a static class with a static accessor method
return {
$: function( name )
{
// implicitly tests that the argument is properly passed
// (would otherwise return `undefined`)
return s[ name ];
},
};
},
keywords = { 'static': true };
val = [ 'value' ],
s = {
// destination object
foo: {
method: function()
{
return val;
},
}
};
assert.strictEqual( val,
sut.standard.wrapProxy( 'foo', null, 0, getInst, 'method', keywords )(),
"Should properly proxy to static membesr via static accessor method"
);
} )();