diff --git a/apps/app/App_Resources/Android/drawable-nodpi/icon.png b/apps/app/App_Resources/Android/drawable-nodpi/icon.png index 76f61ec1f..1034356e2 100644 Binary files a/apps/app/App_Resources/Android/drawable-nodpi/icon.png and b/apps/app/App_Resources/Android/drawable-nodpi/icon.png differ diff --git a/apps/app/App_Resources/Android/drawable-nodpi/testlogo.jpg b/apps/app/App_Resources/Android/drawable-nodpi/testlogo.jpg new file mode 100644 index 000000000..102a9b134 Binary files /dev/null and b/apps/app/App_Resources/Android/drawable-nodpi/testlogo.jpg differ diff --git a/apps/app/ui-tests-app/search-bar/issue-4147.xml b/apps/app/ui-tests-app/search-bar/issue-4147.xml index 79df36dfa..bb3a743d9 100644 --- a/apps/app/ui-tests-app/search-bar/issue-4147.xml +++ b/apps/app/ui-tests-app/search-bar/issue-4147.xml @@ -5,10 +5,10 @@ - - - - - - - \ No newline at end of file + + + + + + + diff --git a/apps/package.json b/apps/package.json index 1b2ffe56f..0ed4081ad 100644 --- a/apps/package.json +++ b/apps/package.json @@ -6,7 +6,7 @@ "nativescript": { "id": "org.nativescript.apps", "tns-android": { - "version": "3.0.0" + "version": "3.0.1" }, "tns-ios": { "version": "3.1.0-2017-5-16-2" diff --git a/tests/.vscode/launch.json b/tests/.vscode/launch.json index c0c6c021b..a51cfc788 100644 --- a/tests/.vscode/launch.json +++ b/tests/.vscode/launch.json @@ -31,7 +31,7 @@ "appRoot": "${workspaceRoot}", "sourceMaps": true, "watch": true, - "stopOnEntry": false, + "stopOnEntry": true, "tnsArgs": [ "--sync-all-files" ] diff --git a/tests/app/TKUnit.ts b/tests/app/TKUnit.ts index 1da82183f..bbe2457a8 100644 --- a/tests/app/TKUnit.ts +++ b/tests/app/TKUnit.ts @@ -210,7 +210,7 @@ export function assertNotEqual(actual: any, expected: any, message?: string) { } if (equals) { - throw new Error(message + " Actual: " + actual + " Expected: " + expected); + throw new Error(message + " Actual: " + actual + " Not_Expected: " + expected); } } @@ -233,59 +233,59 @@ export function assertEqual(actual: T, e /** * Assert two json like objects are deep equal. */ -export function assertDeepEqual(actual, expected, path: any[] = []): void { +export function assertDeepEqual(actual, expected, message: string = '', path: any[] = []): void { let typeofActual: string = typeof actual; let typeofExpected: string = typeof expected; if (typeofActual !== typeofExpected) { - throw new Error("At /" + path.join("/") + " types of actual " + typeofActual + " and expected " + typeofExpected + " differ."); + throw new Error(message + ' ' + "At /" + path.join("/") + " types of actual " + typeofActual + " and expected " + typeofExpected + " differ."); } else if (typeofActual === "object" || typeofActual === "array") { if (expected instanceof Map) { if (actual instanceof Map) { expected.forEach((value, key) => { if (actual.has(key)) { - assertDeepEqual(actual.get(key), value, path.concat([key])); + assertDeepEqual(actual.get(key), value, message, path.concat([key])); } else { - throw new Error("At /" + path.join("/") + " expected Map has key '" + key + "' but actual does not."); + throw new Error(message + ' ' + "At /" + path.join("/") + " expected Map has key '" + key + "' but actual does not."); } }); actual.forEach((value, key) => { if (!expected.has(key)) { - throw new Error("At /" + path.join("/") + " actual Map has key '" + key + "' but expected does not."); + throw new Error(message + ' ' + "At /" + path.join("/") + " actual Map has key '" + key + "' but expected does not."); } }); } else { - throw new Error("At /" + path.join("/") + " expected is Map but actual is not."); + throw new Error(message + ' ' + "At /" + path.join("/") + " expected is Map but actual is not."); } } if (expected instanceof Set) { if (actual instanceof Set) { expected.forEach(i => { if (!actual.has(i)) { - throw new Error("At /" + path.join("/") + " expected Set has item '" + i + "' but actual does not."); + throw new Error(message + ' ' + "At /" + path.join("/") + " expected Set has item '" + i + "' but actual does not."); } }); actual.forEach(i => { if (!expected.has(i)) { - throw new Error("At /" + path.join("/") + " actual Set has item '" + i + "' but expected does not."); + throw new Error(message + ' ' + "At /" + path.join("/") + " actual Set has item '" + i + "' but expected does not."); } }) } else { - throw new Error("At /" + path.join("/") + " expected is Set but actual is not."); + throw new Error(message + ' ' + "At /" + path.join("/") + " expected is Set but actual is not."); } } for (let key in actual) { if (!(key in expected)) { - throw new Error("At /" + path.join("/") + " found unexpected key " + key + "."); + throw new Error(message + ' ' + "At /" + path.join("/") + " found unexpected key " + key + "."); } - assertDeepEqual(actual[key], expected[key], path.concat([key])); + assertDeepEqual(actual[key], expected[key], message, path.concat([key])); } for (let key in expected) { if (!(key in actual)) { - throw new Error("At /" + path.join("/") + " expected a key " + key + "."); + throw new Error(message + ' ' + "At /" + path.join("/") + " expected a key " + key + "."); } } } else if (actual !== expected) { - throw new Error("At /" + path.join("/") + " actual: '" + actual + "' and expected: '" + expected + "' differ."); + throw new Error(message + ' ' + "At /" + path.join("/") + " actual: '" + actual + "' and expected: '" + expected + "' differ."); } } diff --git a/tests/app/app/mainPage.ts b/tests/app/app/mainPage.ts index 81dff086d..a8a52f4f0 100644 --- a/tests/app/app/mainPage.ts +++ b/tests/app/app/mainPage.ts @@ -24,7 +24,7 @@ page.id = "mainPage"; page.on(Page.navigatedToEvent, onNavigatedTo); function runTests() { - setTimeout(() => tests.runAll(), 10); + setTimeout(() => tests.runAll(''), 10); } function onNavigatedTo(args) { diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index ad88bced0..93b934209 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -217,9 +217,6 @@ allTests["HTML-VIEW"] = htmlViewTests; import * as repeaterTests from "./ui/repeater/repeater-tests"; allTests["REPEATER"] = repeaterTests; -import * as searchBarTests from "./ui/search-bar/search-bar-tests"; -allTests["SEARCH-BAR"] = searchBarTests; - import * as segmentedBarTests from "./ui/segmented-bar/segmented-bar-tests"; allTests["SEGMENTED-BAR"] = segmentedBarTests; @@ -238,6 +235,9 @@ if (!(platform.device.os === platform.platformNames.android && parseInt(platform allTests["TANSITIONS"] = transitionTests; } +import * as searchBarTests from "./ui/search-bar/search-bar-tests"; +allTests["SEARCH-BAR"] = searchBarTests; + import * as navigationTests from "./navigation/navigation-tests"; allTests["NAVIGATION"] = navigationTests; diff --git a/tests/app/ui/action-bar/action-bar-tests-common.ts b/tests/app/ui/action-bar/action-bar-tests-common.ts index a06ea2e6a..9925d256b 100644 --- a/tests/app/ui/action-bar/action-bar-tests-common.ts +++ b/tests/app/ui/action-bar/action-bar-tests-common.ts @@ -333,3 +333,7 @@ export function createPageAndNavigate() { return page; } + +export function test_recycling() { + helper.nativeView_recycling_test(() => new actionBarModule.ActionBar()); +} \ No newline at end of file diff --git a/tests/app/ui/activity-indicator/activity-indicator-tests.ts b/tests/app/ui/activity-indicator/activity-indicator-tests.ts index 313bc3a00..f94566fa3 100644 --- a/tests/app/ui/activity-indicator/activity-indicator-tests.ts +++ b/tests/app/ui/activity-indicator/activity-indicator-tests.ts @@ -9,6 +9,10 @@ import * as color from "tns-core-modules/color"; import * as activityIndicatorModule from "tns-core-modules/ui/activity-indicator"; // << activity-indicator-require +export function test_recycling() { + helper.nativeView_recycling_test(() => new activityIndicatorModule.ActivityIndicator()); +} + export function test_default_TNS_values() { // >> activity-indicator-create var indicator = new activityIndicatorModule.ActivityIndicator(); diff --git a/tests/app/ui/border/border-tests.ts b/tests/app/ui/border/border-tests.ts index 3d03ad2c4..c50b618ab 100644 --- a/tests/app/ui/border/border-tests.ts +++ b/tests/app/ui/border/border-tests.ts @@ -1,7 +1,8 @@ // >> border-require import { Border } from "tns-core-modules/ui/border"; // << border-require +import * as helper from "../helper"; -if (Border) { - // NOOP +export function test_recycling() { + helper.nativeView_recycling_test(() => new Border()); } \ No newline at end of file diff --git a/tests/app/ui/button/button-tests.ts b/tests/app/ui/button/button-tests.ts index 1224b4091..7487bcedb 100644 --- a/tests/app/ui/button/button-tests.ts +++ b/tests/app/ui/button/button-tests.ts @@ -16,6 +16,10 @@ import * as bindable from "tns-core-modules/ui/core/bindable"; import * as observable from "tns-core-modules/data/observable"; // << button-require-others +export function test_recycling() { + helper.nativeView_recycling_test(() => new buttonModule.Button()); +} + export var testSetText = function () { helper.buildUIAndRunTest(_createButtonFunc(), _testSetText); } diff --git a/tests/app/ui/date-picker/date-picker-tests.ts b/tests/app/ui/date-picker/date-picker-tests.ts index 812496ddd..73c0a98a5 100644 --- a/tests/app/ui/date-picker/date-picker-tests.ts +++ b/tests/app/ui/date-picker/date-picker-tests.ts @@ -8,6 +8,12 @@ import * as platform from "tns-core-modules/platform"; import * as datePickerModule from "tns-core-modules/ui/date-picker"; // << date-picker-require +import * as helper from "../helper"; + +export function test_recycling() { + helper.nativeView_recycling_test(() => new datePickerModule.DatePicker()); +} + function assertDate(datePicker: datePickerModule.DatePicker, expectedYear: number, expectedMonth: number, expectedDay: number) { TKUnit.assertEqual(datePicker.year, expectedYear, "datePicker.year"); TKUnit.assertEqual(datePicker.month, expectedMonth, "datePicker.month"); diff --git a/tests/app/ui/helper.ts b/tests/app/ui/helper.ts index 3f743c874..59428838c 100644 --- a/tests/app/ui/helper.ts +++ b/tests/app/ui/helper.ts @@ -1,18 +1,18 @@ -import * as view from "tns-core-modules/ui/core/view"; -import * as frame from "tns-core-modules/ui/frame"; -import * as page from "tns-core-modules/ui/page"; -import * as stackLayoutModule from "tns-core-modules/ui/layouts/stack-layout"; -import * as button from "tns-core-modules/ui/button"; +import * as frame from "tns-core-modules/ui/frame"; +import { ViewBase, View, unsetValue, isIOS } from "tns-core-modules/ui/core/view"; +import { Page } from "tns-core-modules/ui/page"; +import { StackLayout } from "tns-core-modules/ui/layouts/stack-layout"; +import { Button } from "tns-core-modules/ui/button"; import * as TKUnit from "../TKUnit"; import * as utils from "tns-core-modules/utils/utils"; -import * as platform from "tns-core-modules/platform"; -import * as colorModule from "tns-core-modules/color"; -import * as formattedStringModule from "tns-core-modules/text/formatted-string"; -import * as spanModule from "tns-core-modules/text/span"; import { ActionBar } from "tns-core-modules/ui/action-bar"; -import { unsetValue } from "tns-core-modules/ui/core/view"; import { Color } from "tns-core-modules/color"; +import { LayoutBase } from "tns-core-modules/ui/layouts/layout-base"; +import { FlexboxLayout } from "tns-core-modules/ui/layouts/flexbox-layout"; +import { FormattedString, Span } from "tns-core-modules/text/formatted-string"; +import { _getProperties, _getStyleProperties } from "tns-core-modules/ui/core/properties"; + var DELTA = 0.1; export var ASYNC = 0.2; @@ -46,7 +46,7 @@ function clearPage(): void { newPage.id = unsetValue; } -export function do_PageTest(test: (views: [page.Page, view.View, view.View, view.View, ActionBar]) => void, content: view.View, secondView: view.View, thirdView: view.View) { +export function do_PageTest(test: (views: [Page, View, View, View, ActionBar]) => void, content: View, secondView: View, thirdView: View) { clearPage(); let newPage = getCurrentPage(); newPage.content = content; @@ -54,20 +54,20 @@ export function do_PageTest(test: (views: [page.Page, view.View, view.View, view newPage.content = null; } -export function do_PageTest_WithButton(test: (views: [page.Page, button.Button, ActionBar]) => void) { +export function do_PageTest_WithButton(test: (views: [Page, Button, ActionBar]) => void) { clearPage(); let newPage = getCurrentPage(); - let btn = new button.Button(); + let btn = new Button(); newPage.content = btn; test([newPage, btn, newPage.actionBar]); newPage.content = null; } -export function do_PageTest_WithStackLayout_AndButton(test: (views: [page.Page, stackLayoutModule.StackLayout, button.Button, ActionBar]) => void) { +export function do_PageTest_WithStackLayout_AndButton(test: (views: [Page, StackLayout, Button, ActionBar]) => void) { clearPage(); let newPage = getCurrentPage(); - let stackLayout = new stackLayoutModule.StackLayout(); - let btn = new button.Button(); + let stackLayout = new StackLayout(); + let btn = new Button(); stackLayout.addChild(btn); newPage.content = stackLayout; test([newPage, stackLayout, btn, newPage.actionBar]); @@ -75,7 +75,7 @@ export function do_PageTest_WithStackLayout_AndButton(test: (views: [page.Page, } //export function buildUIAndRunTest(controlToTest, testFunction, pageCss?, testDelay?) { -export function buildUIAndRunTest(controlToTest: T, testFunction: (views: [T, page.Page]) => void, pageCss?) { +export function buildUIAndRunTest(controlToTest: T, testFunction: (views: [T, Page]) => void, pageCss?) { clearPage(); let newPage = getCurrentPage(); @@ -87,7 +87,7 @@ export function buildUIAndRunTest(controlToTest: T, testFun newPage.css = null; } -export function buildUIWithWeakRefAndInteract(createFunc: () => T, interactWithViewFunc?: (view: T) => void, done?) { +export function buildUIWithWeakRefAndInteract(createFunc: () => T, interactWithViewFunc?: (view: T) => void, done?) { clearPage(); const page = getCurrentPage(); const weakRef = new WeakRef(createFunc()); @@ -120,44 +120,45 @@ export function navigateToModuleAndRunTest(moduleName, context, testFunction) { testFunction(page); } -export function navigate(pageFactory: () => page.Page, navigationContext?: any): page.Page { +export function navigate(pageFactory: () => Page, navigationContext?: any): Page { let entry: frame.NavigationEntry = { create: pageFactory, animated: false, context: navigationContext, clearHistory: true }; return navigateWithEntry(entry); } -export function navigateWithHistory(pageFactory: () => page.Page, navigationContext?: any): page.Page { +export function navigateWithHistory(pageFactory: () => Page, navigationContext?: any): Page { let entry: frame.NavigationEntry = { create: pageFactory, animated: false, context: navigationContext, clearHistory: false }; return navigateWithEntry(entry); } -export function navigateToModule(moduleName: string, context?: any): page.Page { +export function navigateToModule(moduleName: string, context?: any): Page { let entry: frame.NavigationEntry = { moduleName: moduleName, context: context, animated: false, clearHistory: true }; return navigateWithEntry(entry); } -export function getCurrentPage(): page.Page { +export function getCurrentPage(): Page { return frame.topmost().currentPage; } -export function getClearCurrentPage(): page.Page { +export function getClearCurrentPage(): Page { let page = frame.topmost().currentPage; page.style.backgroundColor = unsetValue; page.style.color = unsetValue; page.bindingContext = unsetValue; page.className = unsetValue; page.id = unsetValue; + page.css = ''; return page; } -export function waitUntilNavigatedFrom(oldPage: page.Page) { +export function waitUntilNavigatedFrom(oldPage: Page) { TKUnit.waitUntilReady(() => getCurrentPage() && getCurrentPage() !== oldPage); } -export function waitUntilLayoutReady(view: view.View): void { +export function waitUntilLayoutReady(view: View): void { TKUnit.waitUntilReady(() => view.isLayoutValid); } -export function navigateWithEntry(entry: frame.NavigationEntry): page.Page { +export function navigateWithEntry(entry: frame.NavigationEntry): Page { let page = frame.resolvePageFromEntry(entry); entry.moduleName = null; entry.create = function () { @@ -183,18 +184,18 @@ export function assertAreClose(actual: number, expected: number, message: string TKUnit.assertAreClose(actual, expected, delta, message); } -export function assertViewColor(testView: view.View, hexColor: string) { +export function assertViewColor(testView: View, hexColor: string) { TKUnit.assert(testView.style.color, "Color property not applied correctly. Style value is not defined."); TKUnit.assertEqual(testView.style.color.hex, hexColor, "color property"); } -export function assertViewBackgroundColor(testView: view.ViewBase, hexColor: string) { +export function assertViewBackgroundColor(testView: ViewBase, hexColor: string) { TKUnit.assert(testView.style.backgroundColor, "Background color property not applied correctly. Style value is not defined."); TKUnit.assertEqual(testView.style.backgroundColor.hex, hexColor, "backgroundColor property"); } export function forceGC() { - if (platform.device.os === platform.platformNames.ios) { + if (isIOS) { /* tslint:disable:no-unused-expression */ // Could cause GC on the next call. new ArrayBuffer(4 * 1024 * 1024); @@ -204,29 +205,515 @@ export function forceGC() { TKUnit.wait(0.001); } -export function _generateFormattedString(): formattedStringModule.FormattedString { - let formattedString = new formattedStringModule.FormattedString(); - let span: spanModule.Span; +export function _generateFormattedString(): FormattedString { + let formattedString = new FormattedString(); + let span: Span; - span = new spanModule.Span(); + span = new Span(); span.fontFamily = "serif"; span.fontSize = 10; span.fontWeight = "bold"; - span.color = new colorModule.Color("red"); - span.backgroundColor = new colorModule.Color("blue"); + span.color = new Color("red"); + span.backgroundColor = new Color("blue"); span.textDecoration = "line-through"; span.text = "Formatted"; formattedString.spans.push(span); - span = new spanModule.Span(); + span = new Span(); span.fontFamily = "sans-serif"; span.fontSize = 20; span.fontStyle = "italic"; - span.color = new colorModule.Color("green"); - span.backgroundColor = new colorModule.Color("yellow"); + span.color = new Color("green"); + span.backgroundColor = new Color("yellow"); span.textDecoration = "underline"; span.text = "Text"; formattedString.spans.push(span); return formattedString; } + +const props = _getProperties(); +const styleProps = _getStyleProperties(); +let setters: Map; +let cssSetters: Map; +let defaultNativeGetters: Map any>; + +export function nativeView_recycling_test(createNew: () => View, createLayout?: () => LayoutBase, nativeGetters?: Map any>, customSetters?: Map) { + if (isIOS) { + // recycling not implemented yet. + return; + } + setupSetters(); + const page = getClearCurrentPage(); + let layout: LayoutBase = new FlexboxLayout(); + if (createLayout) { + // This is done on purpose. We need the constructor of Flexbox + // to run otherwise some module fileds stays uninitialized. + layout = createLayout(); + } + + page.content = layout; + + const first = createNew(); + const test = createNew(); + + // Make sure we are not reusing a native views. + first.recycleNativeView = false; + test.recycleNativeView = false; + + page.content = layout; + + layout.addChild(test); + + setValue(test.style, cssSetters); + setValue(test, setters, customSetters); + // Needed so we can reset formattedText + test["secure"] = false; + + const nativeView = test.nativeView; + // Mark so we reuse the native views. + test.recycleNativeView = true; + layout.removeChild(test); + const newer = createNew(); + newer.recycleNativeView = true; + layout.addChild(newer); + layout.addChild(first); + + TKUnit.assertEqual(newer.nativeView, nativeView, "nativeView not reused."); + checkDefaults(newer, first, props, nativeGetters || defaultNativeGetters); + checkDefaults(newer, first, styleProps, nativeGetters || defaultNativeGetters); + layout.removeChild(newer); + layout.removeChild(first); +} + +function checkDefaults(newer: View, first: View, props: Array, nativeGetters: Map any>): void { + props.forEach(prop => { + const name = (prop).name; + if (nativeGetters.has(name)) { + const getter = nativeGetters.get(name); + TKUnit.assertDeepEqual(getter(newer), getter(first), name); + } else if (newer[prop.getDefault]) { + TKUnit.assertDeepEqual(newer[prop.getDefault](), first[prop.getDefault](), name); + } else if (newer[prop.setNative]) { + console.log(`Type: ${newer.typeName} has no getter for ${name} property.`) + } + }); +} + +function setValue(object: Object, setters: Map, customSetters?: Map): void { + setters.forEach((value1, key) => { + let value = customSetters && customSetters.has(key) ? customSetters.get(key) : value1; + const currentValue = object[key]; + if (currentValue === value) { + if (value === 'horizontal' && key === 'orientation') { + // wrap-layout.orientation default value is 'horizontal' + value = 'vertical'; + } else if (value === 2) { + value = 3; + } + } + + object[key] = value; + const newValue = object[key]; + TKUnit.assertNotEqual(newValue, currentValue, `${object} setting ${key} should change current value.`); + }); +} + +function setupSetters(): void { + if (setters) { + return; + } + + setters = new Map(); + // view-base + setters.set('id', "someId"); + setters.set('className', "someClassName"); + setters.set('bindingContext', "someBindingContext"); + + // view + setters.set('automationText', "automationText"); + setters.set('originX', 0.2); + setters.set('originY', 0.2); + setters.set('isEnabled', false); + setters.set('isUserInteractionEnabled', false); + + // action-bar + setters.set('title', 'title'); + setters.set('text', 'text'); + setters.set('icon', '~/logo.png'); + setters.set('visibility', 'collapse'); + + // activity-indicator + setters.set('busy', true); + + // date-picker + setters.set('year', '2010'); + setters.set('month', '2'); + setters.set('day', '2'); + setters.set('maxDate', '2100'); + setters.set('minDate', '2000'); + setters.set('date', new Date(2011, 3, 3)); + + // editable-text + setters.set('keyboardType', 'datetime'); + setters.set('returnKeyType', 'done'); + setters.set('editable', false); + setters.set('updateTextTrigger', 'focusLost'); + setters.set('autocapitalizationType', 'words'); + setters.set('autocorrect', true); + setters.set('hint', 'hint'); + setters.set('maxLength', '10'); + + // html-view + setters.set('html', ''); + + // image-view + setters.set('imageSource', ''); + setters.set('src', ''); + setters.set('loadMode', 'async'); + setters.set('isLoading', true); + setters.set('stretch', 'none'); + + // layout-base + setters.set('clipToBounds', false); + + // absolute-layout + setters.set('left', '20'); + setters.set('top', '20'); + + // dock-layout + setters.set('dock', 'top'); + setters.set('stretchLastChild', false); + + // grid-layout props + setters.set('row', '1'); + setters.set('rowSpan', '2'); + setters.set('col', '1'); + setters.set('colSpan', '2'); + + // stack-layout + setters.set('orientation', 'horizontal'); + + // wrap-layout + // custom orientation value + // setters.set('orientation', 'vertical'); + setters.set('itemWidth', '50'); + setters.set('itemHeight', '50'); + + // list-picker + setters.set('items', ['1', '2', '3']); + setters.set('selectedIndex', '1'); + + // list-view + setters.set('items', ['1', '2', '3']); + setters.set('itemTemplate', '