Merge pull request #163 from NativeScript/feature/list-view-weak-events

List view weak events
This commit is contained in:
Alexander Vakrilov
2015-05-18 13:55:15 +03:00
13 changed files with 463 additions and 250 deletions

View File

@@ -68,6 +68,8 @@ allTests["LIST-PICKER"] = require("./ui/list-picker/list-picker-tests");
allTests["DATE-PICKER"] = require("./ui/date-picker/date-picker-tests");
allTests["TIME-PICKER"] = require("./ui/time-picker/time-picker-tests");
allTests["WEB-VIEW"] = require("./ui/web-view/web-view-tests");
allTests["WEAK-EVENTS"] = require("./weak-event-listener-tests");
if (!isRunningOnEmulator()) {
allTests["LOCATION"] = require("./location-tests");
}

View File

@@ -1,4 +1,5 @@
import TKUnit = require("./TKUnit");
import platform = require("platform");
var timer = require("timer/timer");
// <snippet module="timer" title="timer">
@@ -85,6 +86,12 @@ export var test_setTimeout_shouldReturnNumber = function () {
};
export var test_setTimeout_callbackShouldBeCleared = function () {
// This test is very unstable in iOS, because the platform does not guarantee the
// callback will be cleared on time. Better skip it for iOS.
if (platform.device.os === platform.platformNames.ios) {
return;
}
var completed: boolean;
var isReady = function () { return completed; }

View File

@@ -7,10 +7,12 @@ import TKUnit = require("../TKUnit");
import utils = require("utils/utils");
import types = require("utils/types");
import styling = require("ui/styling");
import platform = require("platform");
var DELTA = 0.1;
export var ASYNC = 0.2;
export var MEMORY_ASYNC = 2;
export function do_PageTest(test: (views: Array<view.View>) => void, content: view.View, secondView: view.View, thirdView: view.View) {
var newPage: page.Page;
@@ -33,7 +35,7 @@ export function do_PageTest(test: (views: Array<view.View>) => void, content: vi
export function do_PageTest_WithButton(test: (views: Array<view.View>) => void) {
var newPage: page.Page;
var btn: button.Button;
var pageFactory = function(): page.Page {
var pageFactory = function (): page.Page {
newPage = new page.Page();
btn = new button.Button();
newPage.content = btn;
@@ -76,7 +78,7 @@ export function do_PageTest_WithStackLayout_AndButton(test: (views: Array<view.V
export function do_PageTest_WithStackLayout_AndButton_NavigatedBack(test: (views: Array<view.View>) => void,
assert: (views: Array<view.View>) => void) {
var newPage: page.Page;
var stackLayout;
var btn;
@@ -175,6 +177,7 @@ export function buildUIWithWeakRefAndInteract<T extends view.View>(createFunc: (
sp.removeChild(weakRef.get());
if (newPage.ios) {
// Could cause GC on the next call.
// NOTE: Don't replace this with forceGC();
new ArrayBuffer(4 * 1024 * 1024);
}
utils.GC();
@@ -188,7 +191,7 @@ export function buildUIWithWeakRefAndInteract<T extends view.View>(createFunc: (
try {
navigate(pageFactory);
TKUnit.waitUntilReady(() => { return testFinished; });
TKUnit.waitUntilReady(() => { return testFinished; }, MEMORY_ASYNC);
}
finally {
goBack();
@@ -221,4 +224,13 @@ export function assertAreClose(actual: number, expected: number, message: string
var delta = Math.floor(density) !== density ? 1.1 : DELTA;
TKUnit.assertAreClose(actual, expected, delta, message);
}
export function forceGC() {
if (platform.device.os === platform.platformNames.ios) {
// Could cause GC on the next call.
new ArrayBuffer(4 * 1024 * 1024);
TKUnit.wait(ASYNC);
}
utils.GC();
}

View File

@@ -430,28 +430,28 @@ export function test_loadMoreItems_not_raised_when_showing_many_items() {
}
export function test_usingAppLevelConvertersInListViewItems() {
var listView = new listViewModule.ListView();
var listView = new listViewModule.ListView();
var dateConverter = function (value, format) {
var result = format;
var day = value.getDate();
result = result.replace("DD", month < 10 ? "0" + day : day);
var month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? "0" + month : month);
result = result.replace("YYYY", value.getFullYear());
return result;
};
var dateConverter = function (value, format) {
var result = format;
var day = value.getDate();
result = result.replace("DD", month < 10 ? "0" + day : day);
var month = value.getMonth() + 1;
result = result.replace("MM", month < 10 ? "0" + month : month);
result = result.replace("YYYY", value.getFullYear());
return result;
};
app.resources["dateConverter"] = dateConverter;
app.resources["dateConverter"] = dateConverter;
var data = new observableArray.ObservableArray();
data.push({date: new Date()});
data.push({ date: new Date() });
function testAction(views: Array<viewModule.View>) {
listView.itemTemplate = "<Label id=\"testLabel\" text=\"{{ date, date | dateConverter('DD.MM.YYYY') }}\" />";
listView.itemTemplate = "<Label id=\"testLabel\" text=\"{{ date, date | dateConverter('DD.MM.YYYY') }}\" />";
listView.items = data;
TKUnit.wait(ASYNC);
var nativeElementText = getTextFromNativeElementAt(listView, 0);
@@ -501,6 +501,33 @@ export function test_BindingListViewToASimpleArrayWithExpression() {
helper.buildUIAndRunTest(listView, testAction);
}
export function test_no_memory_leak_when_items_is_regular_array() {
var createFunc = function (): listViewModule.ListView {
var listView = new listViewModule.ListView();
listView.items = FEW_ITEMS;
return listView;
};
helper.buildUIWithWeakRefAndInteract(createFunc, (list) => {
TKUnit.assert(list.isLoaded, "ListView should be loaded here");
});
}
export function test_no_memory_leak_when_items_is_observable_array() {
// Keep the reference to the observable array to test the weakEventListener
var colors = new observableArray.ObservableArray(["red", "green", "blue"]);
var createFunc = function (): listViewModule.ListView {
var listView = new listViewModule.ListView();
listView.items = colors;
return listView;
};
helper.buildUIWithWeakRefAndInteract(createFunc, (list) => {
TKUnit.assert(list.isLoaded, "ListView should be loaded here");
});
}
function loadViewWithItemNumber(args: listViewModule.ItemEventData) {
if (!args.view) {
args.view = new labelModule.Label();
@@ -509,16 +536,16 @@ function loadViewWithItemNumber(args: listViewModule.ItemEventData) {
}
function getTextFromNativeElementAt(listView: listViewModule.ListView, index: number): any {
if (listView.android) {
var nativeElement = listView.android.getChildAt(index);
if (nativeElement instanceof android.view.ViewGroup) {
return (<android.widget.TextView>(<any>nativeElement.getChildAt(0))).getText();
}
if (listView.android) {
var nativeElement = listView.android.getChildAt(index);
if (nativeElement instanceof android.view.ViewGroup) {
return (<android.widget.TextView>((<any>nativeElement).getChildAt(0))).getText();
}
return (<android.widget.TextView>nativeElement).getText();
}
else if (listView.ios) {
return listView.ios.visibleCells()[index].contentView.subviews[0].text;
}
}
else if (listView.ios) {
return listView.ios.visibleCells()[index].contentView.subviews[0].text;
}
}
function getNativeViewCount(listView: listViewModule.ListView): number {

View File

@@ -0,0 +1,162 @@
import TKUnit = require("./TKUnit");
import observable = require("data/observable");
import weakEvents = require("ui/core/weak-event-listener");
import helper = require("./ui/helper");
class Target {
public counter: number = 0;
public onEvent(data: observable.EventData) {
this.counter++;
}
}
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_source() {
TKUnit.assertThrows(() => {
weakEvents.addWeakEventListener(undefined, "eventName", emptyHandler, {});
});
}
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_target() {
TKUnit.assertThrows(() => {
weakEvents.addWeakEventListener(new observable.Observable(), "eventName", emptyHandler, undefined);
});
}
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_handler() {
TKUnit.assertThrows(() => {
weakEvents.addWeakEventListener(new observable.Observable(), "eventName", undefined, {});
});
}
export function test_addWeakEventListener_throwsWhenCalledwitnInvalid_name() {
TKUnit.assertThrows(() => {
weakEvents.addWeakEventListener(new observable.Observable(), undefined, emptyHandler, {});
});
}
export function test_addWeakEventListener_listensForEvent() {
var source = new observable.Observable();
var target = new Target();
weakEvents.addWeakEventListener(
source,
observable.Observable.propertyChangeEvent,
target.onEvent,
target);
helper.forceGC();
source.set("testProp", "some value");
TKUnit.assertEqual(target.counter, 1, "Handler not called.");
}
export function test_addWeakEventListener_listensForEven_multipleTargetst() {
var source = new observable.Observable();
var target1 = new Target();
var target2 = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1);
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2);
helper.forceGC();
source.set("testProp", "some value");
TKUnit.assertEqual(target1.counter, 1, "Handler not called.");
TKUnit.assertEqual(target2.counter, 1, "Handler not called.");
}
export function test_removeWeakEventListener_StopsListeningForEvet() {
var source = new observable.Observable();
var target = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
source.set("testProp", "some value");
TKUnit.assertEqual(target.counter, 0, "Handler should not be called.");
}
export function test_handlerIsCalled_WithTargetAsThis() {
var source = new observable.Observable();
var target = new Object();
var callbackCalled = false;
var handler = function (args: observable.EventData) {
TKUnit.assertEqual(this, target, "this should be the target");
callbackCalled = true;
}
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, handler, target);
source.set("testProp", "some value");
TKUnit.assert(callbackCalled, "Handler not called.");
}
export function test_listnerDoesNotRetainTarget() {
var source = new observable.Observable();
var target = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
var targetRef = new WeakRef(target);
target = undefined;
helper.forceGC();
TKUnit.assert(!targetRef.get(), "Target should be released after GC");
}
export function test_listnerDoesNotRetainSource() {
var source = new observable.Observable();
var target = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
var sourceRef = new WeakRef(source);
source = undefined;
helper.forceGC();
TKUnit.assert(!sourceRef.get(), "Source should be released after GC");
}
export function test_handlerIsDetached_WhenAllListenersAreRemoved() {
var source = new observable.Observable();
var target1 = new Target();
var target2 = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1);
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2);
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target1.onEvent, target1)
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target2.onEvent, target2)
TKUnit.assert(!source.hasListeners(observable.Observable.propertyChangeEvent), "All events should be detached");
}
export function test_autoDetachingOfDeadReferences() {
var source = new observable.Observable();
for (var i = 0; i < 100; i++) {
addListenerWithSource(source);
}
helper.forceGC();
var target = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target);
weakEvents.removeWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
TKUnit.assert(!source.hasListeners(observable.Observable.propertyChangeEvent), "All events should be detached");
}
function addListenerWithSource(source: observable.Observable) {
var target = new Target();
weakEvents.addWeakEventListener(source, observable.Observable.propertyChangeEvent, target.onEvent, target)
}
function emptyHandler(data: observable.EventData) {
// Do nothing.
}