From 2c2ce3743e817d38a8c353ac12ccedff0bbf5b3f Mon Sep 17 00:00:00 2001 From: Vasil Chimev Date: Thu, 14 Nov 2019 10:04:18 +0200 Subject: [PATCH] feat: expose testing module --- nativescript-core/testing/tk-unit.ts | 464 ++++++++++++++ nativescript-core/testing/ui-helper.ts | 856 +++++++++++++++++++++++++ 2 files changed, 1320 insertions(+) create mode 100644 nativescript-core/testing/tk-unit.ts create mode 100644 nativescript-core/testing/ui-helper.ts diff --git a/nativescript-core/testing/tk-unit.ts b/nativescript-core/testing/tk-unit.ts new file mode 100644 index 000000000..2c92273bb --- /dev/null +++ b/nativescript-core/testing/tk-unit.ts @@ -0,0 +1,464 @@ +/* tslint:disable */ + +/* Notes: + + 1. all test function names should begin with 'test' + 2. (if exists) at the beginning of module test setUpModule() module function is called + 3. (if exists) at the beginning of each test setUp() module function is called + 4. tests should use TKUnit.assert(condition, message) to mark error. If no assert fails test is successful + 5. (if exists) at the end of each test tearDown() module function is called + 6. (if exists) at the end of module test tearDownModule() module function is called + +*/ + +import * as application from "../application"; +import * as trace from "../trace"; +import * as platform from "../platform"; +import * as types from "../utils/types"; +import * as timer from "../timer"; + +const sdkVersion = parseInt(platform.device.sdkVersion); + +trace.enable(); + +export interface TestInfoEntry { + testFunc: () => void; + instance: Object; + isTest: boolean; + testName: string; + isPassed: boolean; + errorMessage: string; + testTimeout: number; + duration: number; +} + +export function time(): number { + if (global.android) { + // return java.lang.System.nanoTime() / 1000000; // 1 ms = 1000000 ns + } else { + return CACurrentMediaTime() * 1000; + } +} + +export function write(message: string, type?: number) { + trace.write(message, trace.categories.Test, type); +} + +function runTest(testInfo: TestInfoEntry) { + let start = time(); + let duration; + try { + if (testInfo.instance) { + testInfo.testFunc.apply(testInfo.instance); + } else { + testInfo.testFunc(); + } + + if (testInfo.isTest) { + duration = time() - start; + testInfo.duration = duration; + write(`--- [${testInfo.testName}] OK, duration: ${duration.toFixed(2)}`, trace.messageType.info); + testInfo.isPassed = true; + } + } + catch (e) { + if (testInfo.isTest) { + duration = time() - start; + testInfo.duration = duration; + write(`--- [${testInfo.testName}] FAILED: ${e.message}, Stack: ${e.stack}, duration: ${duration.toFixed(2)}`, trace.messageType.error); + testInfo.isPassed = false; + testInfo.errorMessage = e.message; + } + } +} + +export interface TestFailure { + moduleName: string; + testName: string; + errorMessage: string; +} + +export interface TestModuleRunResult { + name: string; + count: number; + succeeded: number; + failed: Array; +} + +let testsQueue: Array; +const defaultTimeout = 5000; + +function runAsync(testInfo: TestInfoEntry, recursiveIndex: number, testTimeout?: number) { + let error; + let isDone = false; + let handle; + const testStartTime = time(); + //write("--- [" + testInfo.testName + "] Started at: " + testStartTime, trace.messageType.info); + const doneCallback = (e: Error) => { + if (e) { + error = e; + } else { + isDone = true; + } + } + + const timeout = testTimeout || testInfo.testTimeout || defaultTimeout; + + let duration; + const checkFinished = () => { + duration = time() - testStartTime; + testInfo.duration = duration; + if (isDone) { + write(`--- [${testInfo.testName}] OK, duration: ${duration.toFixed(2)}`, trace.messageType.info); + testInfo.isPassed = true; + runTests(testsQueue, recursiveIndex + 1); + } else if (error) { + write(`--- [${testInfo.testName}] FAILED: ${error.message}, duration: ${duration.toFixed(2)}`, trace.messageType.error); + testInfo.errorMessage = error.message; + runTests(testsQueue, recursiveIndex + 1); + } else { + const testEndTime = time(); + if (testEndTime - testStartTime > timeout) { + write(`--- [${testInfo.testName}] TIMEOUT, duration: ${duration.toFixed(2)}`, trace.messageType.error); + testInfo.errorMessage = "Test timeout."; + runTests(testsQueue, recursiveIndex + 1); + } else { + setTimeout(checkFinished, 20); + } + } + } + + try { + if (testInfo.instance) { + testInfo.testFunc.apply(testInfo.instance, [doneCallback]); + } else { + const func: any = testInfo.testFunc; + func(doneCallback); + } + } catch (e) { + doneCallback(e); + } + + setTimeout(checkFinished, 20); +} + +export function runTests(tests: Array, recursiveIndex) { + testsQueue = tests; + + for (let i = recursiveIndex; i < testsQueue.length; i++) { + const testEntry = testsQueue[i]; + + if (testEntry.testFunc.length > 0) { + return runAsync(testEntry, i); + } else { + runTest(testEntry); + } + } +} + +export function assert(test: any, message?: string) { + if (!test) { + throw new Error(message); + } +} + +export function assertTrue(test: boolean, message?: string) { + if (test !== true) { + throw new Error(message); + } +} + +export function assertFalse(test: boolean, message?: string) { + if (test !== false) { + throw new Error(message); + } +} + +export function assertNotEqual(actual: any, expected: any, message?: string) { + let equals = false; + if (types.isUndefined(actual) && types.isUndefined(expected)) { + equals = true; + } else if (!types.isNullOrUndefined(actual) && !types.isNullOrUndefined(expected)) { + if (types.isFunction(actual.equals)) { + // Use the equals method + if (actual.equals(expected)) { + equals = true; + } + } else { + equals = actual === expected; + } + } + + if (equals) { + throw new Error(message + " Actual: " + actual + " Not_Expected: " + expected); + } +} + +export function assertEqual(actual: T, expected: T, message: string = '') { + if (!types.isNullOrUndefined(actual) + && !types.isNullOrUndefined(expected) + && types.getClass(actual) === types.getClass(expected) + && types.isFunction(actual.equals)) { + + // Use the equals method + if (!actual.equals(expected)) { + throw new Error(`${message} Actual: <${actual}>(${typeof (actual)}). Expected: <${expected}>(${typeof (expected)})`); + } + } + else if (actual !== expected) { + throw new Error(`${message} Actual: <${actual}>(${typeof (actual)}). Expected: <${expected}>(${typeof (expected)})`); + } +} + +/** + * Assert two json like objects are deep equal. + */ +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(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, message, path.concat([key])); + } else { + 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(message + ' ' + "At /" + path.join("/") + " actual Map has key '" + key + "' but expected does not."); + } + }); + } else { + 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(message + ' ' + "At /" + path.join("/") + " expected Set has item '" + i + "' but actual does not."); + } + }); + actual.forEach(i => { + if (!expected.has(i)) { + throw new Error(message + ' ' + "At /" + path.join("/") + " actual Set has item '" + i + "' but expected does not."); + } + }) + } else { + 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(message + ' ' + "At /" + path.join("/") + " found unexpected key " + key + "."); + } + assertDeepEqual(actual[key], expected[key], message, path.concat([key])); + } + for (let key in expected) { + if (!(key in actual)) { + throw new Error(message + ' ' + "At /" + path.join("/") + " expected a key " + key + "."); + } + } + } else if (actual !== expected) { + throw new Error(message + ' ' + "At /" + path.join("/") + " actual: '" + actual + "' and expected: '" + expected + "' differ."); + } +} + +export function assertDeepSuperset(actual, expected, 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."); + } else if (typeofActual === "object" || typeofActual === "array") { + for (let key in expected) { + if (!(key in actual)) { + throw new Error("At /" + path.join("/") + " expected a key " + key + "."); + } + assertDeepSuperset(actual[key], expected[key], path.concat([key])); + } + } else if (actual !== expected) { + throw new Error("At /" + path.join("/") + " actual: '" + actual + "' and expected: '" + expected + "' differ."); + } +} + +export function assertNull(actual: any, message?: string) { + if (actual !== null && actual !== undefined) { + throw new Error(message + " Actual: " + actual + " is not null/undefined"); + } +} + +export function assertNotNull(actual: any, message?: string) { + if (actual === null || actual === undefined) { + throw new Error(message + " Actual: " + actual + " is null/undefined"); + } +} + +export function areClose(actual: number, expected: number, delta: number): boolean { + if (isNaN(actual) || Math.abs(actual - expected) > delta) { + return false; + } + + return true; +} + +export function assertAreClose(actual: number, expected: number, delta: number, message?: string) { + if (!areClose(actual, expected, delta)) { + throw new Error(message + " Numbers are not close enough. Actual: " + actual + " Expected: " + expected + " Delta: " + delta); + } +} + +export function assertMatches(actual: string, expected: RegExp, message?: string) { + if (expected.test(actual) !== true) { + throw new Error(`"${actual}" doesn't match "${expected}". ${message}`); + } +} + +export function arrayAssert(actual: Array, expected: Array, message?: string) { + if (actual.length !== expected.length) { + throw new Error(message + " Actual array length: " + actual.length + " Expected array length: " + expected.length); + } + + for (let i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) { + throw new Error(message + " Actual element at " + i + " is: " + actual[i] + " Expected element is: " + expected[i]); + } + } +} + +export function assertThrows(testFunc: () => void, assertMessage?: string, expectedMessage?: string) { + const re = expectedMessage ? new RegExp(`^${expectedMessage}$`) : null; + return assertThrowsRegExp(testFunc, assertMessage, re); +} + +export function assertThrowsRegExp(testFunc: () => void, assertMessage?: string, expectedMessage?: RegExp) { + let actualError: Error; + try { + testFunc(); + } catch (e) { + actualError = e; + } + + if (!actualError) { + throw new Error("Missing expected exception. " + assertMessage); + } + + if (expectedMessage && !expectedMessage.test(actualError.message)) { + throw new Error("Got unwanted exception. Actual error: " + actualError.message + " Expected to match: " + expectedMessage); + } +} + +export function wait(seconds: number): void { + waitUntilReady(() => false, seconds, false); +} + +export function waitUntilReady(isReady: () => boolean, timeoutSec: number = 3, shouldThrow: boolean = true) { + if (!isReady) { + return; + } + + if (application.ios) { + const timeoutMs = timeoutSec * 1000; + let totalWaitTime = 0; + while (true) { + const begin = time(); + const currentRunLoop = NSRunLoop.currentRunLoop; + currentRunLoop.limitDateForMode(currentRunLoop.currentMode); + if (isReady()) { + break; + } + + totalWaitTime += (time() - begin); + if (totalWaitTime >= timeoutMs) { + if (shouldThrow) { + throw new Error("waitUntilReady Timeout."); + } else { + break; + } + } + } + } else if (application.android) { + doModalAndroid(isReady, timeoutSec, shouldThrow); + } +} + +// Setup for the Android modal loop implementation +// TODO: If these platform-specific implementations continue to grow, think of per-platform separation (TKUnit.android) +let nextMethod; +let targetField; +let prepared; + +function prepareModal() { + if (prepared) { + return; + } + + const clsMsgQueue = java.lang.Class.forName("android.os.MessageQueue"); + const clsMsg = java.lang.Class.forName("android.os.Message"); + const methods = clsMsgQueue.getDeclaredMethods(); + for (let i = 0; i < methods.length; i++) { + if (methods[i].getName() === "next") { + nextMethod = methods[i]; + nextMethod.setAccessible(true); + break; + } + } + + const fields = clsMsg.getDeclaredFields(); + for (let i = 0; i < fields.length; i++) { + if (fields[i].getName() === "target") { + targetField = fields[i]; + targetField.setAccessible(true); + break; + } + } + + prepared = true; +} + +function doModalAndroid(quitLoop: () => boolean, timeoutSec: number, shouldThrow: boolean = true) { + if (!quitLoop) { + return; + } + + prepareModal(); + + const queue = android.os.Looper.myQueue(); + + let quit = false; + let timeout = false; + timer.setTimeout(() => { + quit = true; + timeout = true; + }, timeoutSec * 1000); + + let msg; + + while (!quit) { + msg = nextMethod.invoke(queue, null); + if (msg) { + const target = targetField.get(msg); + if (!target) { + quit = true; + } else { + target.dispatchMessage(msg); + } + + if (sdkVersion < 21) {//https://code.google.com/p/android-test-kit/issues/detail?id=84 + msg.recycle(); + } + } + + if (shouldThrow && timeout) { + throw new Error("waitUntilReady Timeout."); + } + + if (!quit && quitLoop()) { + quit = true; + } + } +} diff --git a/nativescript-core/testing/ui-helper.ts b/nativescript-core/testing/ui-helper.ts new file mode 100644 index 000000000..2457362f1 --- /dev/null +++ b/nativescript-core/testing/ui-helper.ts @@ -0,0 +1,856 @@ +import { Color } from "../color"; +import { FormattedString, Span } from "../text/formatted-string"; +import { ActionBar } from "../ui/action-bar"; +// TODO: Remove this and get it from global to decouple builder for angular +import { Builder } from "../ui/builder"; +import { Button } from "../ui/button"; +import { isIOS, unsetValue, View, ViewBase } from "../ui/core/view"; +import { Frame, NavigationEntry } from "../ui/frame"; +import { LayoutBase } from "../ui/layouts/layout-base"; +import { StackLayout } from "../ui/layouts/stack-layout"; +import { Page } from "../ui/page"; +import { TabView, TabViewItem } from "../ui/tab-view"; +import * as utils from "../utils/utils"; + +import * as TKUnit from "./tk-unit"; + +const DELTA = 0.1; + +export let ASYNC = 0.2; +export let MEMORY_ASYNC = 2; + +export function getColor(uiColor: UIColor): Color { + let redRef = new interop.Reference(); + let greenRef = new interop.Reference(); + let blueRef = new interop.Reference(); + let alphaRef = new interop.Reference(); + + uiColor.getRedGreenBlueAlpha(redRef, greenRef, blueRef, alphaRef); + let red = redRef.value * 255; + let green = greenRef.value * 255; + let blue = blueRef.value * 255; + let alpha = alphaRef.value * 255; + + return new Color(alpha, red, green, blue); +} + +export function clearPage(): void { + let newPage = getCurrentPage(); + if (!newPage) { + throw new Error("NO CURRENT PAGE!!!!"); + } + + newPage.style.backgroundColor = unsetValue; + newPage.style.color = unsetValue; + newPage.bindingContext = unsetValue; + newPage.className = unsetValue; + newPage.id = unsetValue; +} + +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; + test([newPage, content, secondView, thirdView, newPage.actionBar]); + newPage.content = null; +} + +export function do_PageTest_WithButton(test: (views: [Page, Button, ActionBar]) => void) { + clearPage(); + let newPage = getCurrentPage(); + let btn = new Button(); + newPage.content = btn; + test([newPage, btn, newPage.actionBar]); + newPage.content = null; +} + +export function do_PageTest_WithStackLayout_AndButton(test: (views: [Page, StackLayout, Button, ActionBar]) => void) { + clearPage(); + let newPage = getCurrentPage(); + let stackLayout = new StackLayout(); + let btn = new Button(); + stackLayout.addChild(btn); + newPage.content = stackLayout; + test([newPage, stackLayout, btn, newPage.actionBar]); + newPage.content = null; +} + +export interface PageOptions { + pageCss?: any; + actionBar?: boolean; + actionBarFlat?: boolean; + actionBarHidden?: boolean; + tabBar?: boolean; +} + +export function buildUIAndRunTest(controlToTest: T, testFunction: (views: [T, Page]) => void, options?: PageOptions) { + clearPage(); + let newPage = getCurrentPage(); + + let testSubject = controlToTest as View; + + if (options) { + if (options.pageCss) { + newPage.css = options.pageCss; + } + + newPage.actionBarHidden = true; + newPage.actionBar.flat = false; + + if (options.actionBar) { + newPage.actionBarHidden = false; + newPage.actionBar.title = "Test ActionBar"; + } + + if (options.actionBarFlat) { + newPage.actionBarHidden = false; + newPage.actionBar.title = "Test ActionBar Flat"; + newPage.actionBar.flat = true; + } + + if (options.actionBarHidden) { + newPage.actionBarHidden = true; + } + + if (options.tabBar) { + const tabView = new TabView(); + const tabEntry = new TabViewItem(); + tabEntry.title = "Test"; + tabEntry.view = controlToTest; + tabView.items = [tabEntry]; + testSubject = tabView; + } + } + + newPage.content = testSubject; + + testFunction([controlToTest, newPage]); + newPage.content = null; + newPage.css = null; +} + +export function buildUIWithWeakRefAndInteract(createFunc: () => T, interactWithViewFunc?: (view: T) => void, done?) { + clearPage(); + const page = getCurrentPage(); + const weakRef = new WeakRef(createFunc()); + page.content = weakRef.get(); + if (interactWithViewFunc) { + interactWithViewFunc(weakRef.get()); + } + page.content = null; + // Give a change for native cleanup (e.g. keyboard close, etc.). + TKUnit.wait(0.001); + if (page.ios) { + /* tslint:disable:no-unused-expression */ + // Could cause GC on the next call. + // NOTE: Don't replace this with forceGC(); + new ArrayBuffer(4 * 1024 * 1024); + + // An additional GC and wait are needed since WebKit upgrade to version 12.0 + // (TEXT-FIELD.testMemoryLeak test started failing sporadically) + utils.GC(); + TKUnit.wait(0.1); + } + utils.GC(); + try { + TKUnit.assert(!weakRef.get(), weakRef.get() + " leaked!"); + done(null); + } + catch (ex) { + done(ex); + } +} + +export function navigateToModuleAndRunTest(moduleName, context, testFunction) { + let page = navigateToModule(moduleName, context); + testFunction(page); +} + +export function navigate(pageFactory: () => Page, navigationContext?: any): Page { + let entry: NavigationEntry = { create: pageFactory, animated: false, context: navigationContext, clearHistory: true }; + + return navigateWithEntry(entry); +} + +export function navigateWithHistory(pageFactory: () => Page, navigationContext?: any): Page { + let entry: NavigationEntry = { create: pageFactory, animated: false, context: navigationContext, clearHistory: false }; + + return navigateWithEntry(entry); +} + +export function navigateToModule(moduleName: string, context?: any): Page { + let entry: NavigationEntry = { moduleName: moduleName, context: context, animated: false, clearHistory: true }; + + return navigateWithEntry(entry); +} + +export function getCurrentPage(): Page { + return Frame.topmost().currentPage; +} + +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 waitUntilNavigatedTo(page: Page, action: Function) { + let completed = false; + function navigatedTo(args) { + args.object.page.off("navigatedTo", navigatedTo); + completed = true; + } + + page.on("navigatedTo", navigatedTo); + action(); + TKUnit.waitUntilReady(() => completed, 5); +} + +export function waitUntilNavigatedFrom(action: Function, topFrame?: Frame) { + const currentPage = topFrame ? topFrame.currentPage : Frame.topmost().currentPage; + let completed = false; + function navigatedFrom(args) { + args.object.page.off("navigatedFrom", navigatedFrom); + completed = true; + } + + currentPage.on("navigatedFrom", navigatedFrom); + action(); + TKUnit.waitUntilReady(() => completed); +} + +export function waitUntilLayoutReady(view: View): void { + TKUnit.waitUntilReady(() => view.isLayoutValid); +} + +export function navigateWithEntry(entry: NavigationEntry, topFrame?: Frame): Page { + const page = Builder.createViewFromEntry(entry) as Page; + entry.moduleName = null; + entry.create = function () { + return page; + }; + + waitUntilNavigatedFrom(() => topFrame ? topFrame.navigate(entry) : Frame.topmost().navigate(entry)); + + return page; +} + +export function goBack(topFrame?: Frame) { + waitUntilNavigatedFrom(() => topFrame ? topFrame.goBack() : Frame.topmost().goBack()); +} + +export function assertAreClose(actual: number, expected: number, message: string): void { + const density = utils.layout.getDisplayDensity(); + const delta = Math.floor(density) !== density ? 1.1 : DELTA; + + TKUnit.assertAreClose(actual, expected, delta, message); +} + +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: 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 assertTabSelectedTabTextColor(testView: ViewBase, hexColor: string) { + TKUnit.assert(testView.style.selectedTabTextColor, "selectedTabTextColor property not applied correctly. Style value is not defined."); + TKUnit.assertEqual(testView.style.selectedTabTextColor.hex, hexColor, "selectedTabTextColor property not applied correctly"); +} + +export function forceGC() { + if (isIOS) { + /* tslint:disable:no-unused-expression */ + // Could cause GC on the next call. + new ArrayBuffer(4 * 1024 * 1024); + } + + utils.GC(); + TKUnit.wait(0.001); +} + +export function _generateFormattedString(): FormattedString { + let formattedString = new FormattedString(); + let span: Span; + + span = new Span(); + span.fontFamily = "serif"; + span.fontSize = 10; + span.fontWeight = "bold"; + span.color = new Color("red"); + span.backgroundColor = new Color("blue"); + span.textDecoration = "line-through"; + span.text = "Formatted"; + formattedString.spans.push(span); + + span = new Span(); + span.fontFamily = "sans-serif"; + span.fontSize = 20; + span.fontStyle = "italic"; + span.color = new Color("green"); + span.backgroundColor = new Color("yellow"); + span.textDecoration = "underline"; + span.text = "Text"; + formattedString.spans.push(span); + + return formattedString; +} + +export function nativeView_recycling_test(createNew: () => View, createLayout?: () => LayoutBase, nativeGetters?: Map any>, customSetters?: Map) { + return; + + // 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 = "never"; + // test.recycleNativeView = "never"; + + // 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.nativeViewProtected; + // // Mark so we reuse the native views. + // test.recycleNativeView = "always"; + // layout.removeChild(test); + // const newer = createNew(); + // newer.recycleNativeView = "always"; + // layout.addChild(newer); + // layout.addChild(first); + + // if (first.typeName !== "SearchBar") { + // // There are way too many differences in native methods for search-bar. + // // There are too many methods that just throw for newly created views in API lvl 19 and 17 + // if (sdkVersion < 21) { + // TKUnit.waitUntilReady(() => layout.isLayoutValid); + // } + + // compareUsingReflection(newer, first); + // } + + // TKUnit.assertEqual(newer.nativeViewProtected, nativeView, "nativeView not reused."); + // checkDefaults(newer, first, props, nativeGetters || defaultNativeGetters); + // checkDefaults(newer, first, styleProps, nativeGetters || defaultNativeGetters); + + // layout.removeChild(newer); + // layout.removeChild(first); +} + +// function compareUsingReflection(recycledNativeView: View, newNativeView: View): void { +// const recycled: android.view.View = recycledNativeView.nativeViewProtected; +// const newer: android.view.View = newNativeView.nativeViewProtected; +// TKUnit.assertNotEqual(recycled, newer); +// const methods = newer.getClass().getMethods(); +// for (let i = 0, length = methods.length; i < length; i++) { +// const method = methods[i]; +// const returnType = method.getReturnType(); +// const name = method.getName(); +// const skip = name.includes("ViewId") +// || name.includes("Accessibility") +// || name.includes("hashCode") +// || name === "getId" +// || name === "hasFocus" +// || name === "isDirty" +// || name === "getLeft" +// || name === "getTop" +// || name === "getRight" +// || name === "getBottom" +// || name === "getWidth" +// || name === "getHeight" +// || name === "getX" +// || name === "getY" +// || name.includes("getMeasured") +// || name === "toString"; + +// if (skip || method.getParameterTypes().length > 0) { +// continue; +// } + +// if ((java).lang.Comparable.class.isAssignableFrom(returnType)) { +// const defValue = method.invoke(newer, null); +// const currValue = method.invoke(recycled, null); +// if (defValue !== currValue && defValue.compareTo(currValue) !== 0) { +// throw new Error(`Actual: ${currValue}, Expected: ${defValue}, for method: ${method.getName()}`); +// } +// } else if (java.lang.String.class === returnType || +// java.lang.Character.class === returnType || +// (java).lang.CharSequence.class === returnType || +// returnType === java.lang.Byte.TYPE || +// returnType === java.lang.Double.TYPE || +// returnType === java.lang.Float.TYPE || +// returnType === java.lang.Integer.TYPE || +// returnType === java.lang.Long.TYPE || +// returnType === java.lang.Short.TYPE || +// returnType === java.lang.Boolean.TYPE) { + +// const defValue = method.invoke(newer, null); +// const currValue = method.invoke(recycled, null); +// if ((currValue + "") !== (defValue + "")) { +// throw new Error(`Actual: ${currValue}, Expected: ${defValue}, for method: ${method.getName()}`); +// } +// } +// } +// } + +// 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", "