diff --git a/tests/app/app/main-page.css b/tests/app/app/main-page.css new file mode 100644 index 000000000..68d098e0c --- /dev/null +++ b/tests/app/app/main-page.css @@ -0,0 +1,3 @@ +Button { + color: green; +} diff --git a/tests/app/livesync/livesync-tests.ts b/tests/app/livesync/livesync-tests.ts index f1d0a4007..da1cc9b2c 100644 --- a/tests/app/livesync/livesync-tests.ts +++ b/tests/app/livesync/livesync-tests.ts @@ -15,6 +15,7 @@ const mainPageCssFileName = "./app/main-page.css"; const mainPageHtmlFileName = "./app/main-page.html"; const mainPageXmlFileName = "./app/main-page.xml"; +const black = new Color("black"); const green = new Color("green"); const mainPageTemplate = ` @@ -56,7 +57,7 @@ export function test_onLiveSync_ModuleContext_Script_AppTs() { } export function test_onLiveSync_ModuleContext_Style_MainPageCss() { - _test_onLiveSync_ModuleContext({ type: "style", path: mainPageCssFileName }); + _test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName }); } export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() { @@ -110,4 +111,29 @@ function _test_onLiveSync_ModuleContext(context: { type, path }) { const topmostFrame = frame.topmost(); TKUnit.waitUntilReady(() => topmostFrame.currentPage && topmostFrame.currentPage.isLoaded && !topmostFrame.canGoBack()); TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded); +} + +function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) { + const pageBeforeNavigation = helper.getCurrentPage(); + + const page = parse(pageTemplate); + helper.navigateWithHistory(() => page); + + const pageBeforeLiveSync = helper.getCurrentPage(); + pageBeforeLiveSync._moduleName = "main-page"; + global.__onLiveSync({ type: context.type, path: context.path }); + + const pageAfterLiveSync = helper.getCurrentPage(); + TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString()); + + TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Local styles NOT applied - livesync navigation executed!"); + TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!"); + TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!"); + + helper.goBack(); + + const pageAfterNavigationBack = helper.getCurrentPage(); + TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, black, "App styles applied on back navigation!"); + TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!"); + TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!"); } \ No newline at end of file diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index f79e2eb44..aef985b13 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -168,12 +168,15 @@ allTests["VISUAL-STATE"] = visualStateTests; import * as valueSourceTests from "./ui/styling/value-source-tests"; allTests["VALUE-SOURCE"] = valueSourceTests; -import * as buttonTests from "./ui/button/button-tests"; -allTests["BUTTON"] = buttonTests; - import * as borderTests from "./ui/border/border-tests"; allTests["BORDER"] = borderTests; +import * as builderTests from "./ui/builder/builder-tests"; +allTests["BUILDER"] = builderTests; + +import * as buttonTests from "./ui/button/button-tests"; +allTests["BUTTON"] = buttonTests; + import * as labelTests from "./ui/label/label-tests"; allTests["LABEL"] = labelTests; diff --git a/tests/app/ui/builder/builder-tests.ts b/tests/app/ui/builder/builder-tests.ts new file mode 100644 index 000000000..bfb65a2d0 --- /dev/null +++ b/tests/app/ui/builder/builder-tests.ts @@ -0,0 +1,26 @@ +import { path } from "tns-core-modules/file-system"; +import { loadPage } from "tns-core-modules/ui/builder"; +import { assertEqual, assertNull } from "../../TKUnit"; + +const COMPONENT_MODULE = "component-module"; +const LABEL = "label"; + +function getViewComponent() { + const moduleNamePath = path.join(__dirname, COMPONENT_MODULE); + const fileName = path.join(__dirname, `${COMPONENT_MODULE}.xml`); + const view = loadPage(moduleNamePath, fileName); + return view; +} + +export function test_view_is_module_root_component() { + const view = getViewComponent(); + const actualModule = view._moduleName; + assertEqual(actualModule, COMPONENT_MODULE, `View<${view}> is NOT root component of module <${COMPONENT_MODULE}>.`); +} + +export function test_view_is_NOT_module_root_component() { + const view = getViewComponent(); + const nestedView = view.getViewById(`${LABEL}`); + const undefinedModule = nestedView._moduleName; + assertNull(undefinedModule, `View<${nestedView}> should NOT be a root component of a module.`); +} diff --git a/tests/app/ui/builder/component-module.xml b/tests/app/ui/builder/component-module.xml new file mode 100644 index 000000000..ec78523a2 --- /dev/null +++ b/tests/app/ui/builder/component-module.xml @@ -0,0 +1,3 @@ + + + diff --git a/tns-core-modules/ui/builder/builder.ts b/tns-core-modules/ui/builder/builder.ts index 86efcb94a..4d4b3e5c1 100644 --- a/tns-core-modules/ui/builder/builder.ts +++ b/tns-core-modules/ui/builder/builder.ts @@ -59,7 +59,9 @@ export function load(pathOrOptions: string | LoadOptions, context?: any): View { export function loadPage(moduleNamePath: string, fileName: string, context?: any): View { const componentModule = loadInternal(fileName, context, moduleNamePath); - return componentModule && componentModule.component; + const componentView = componentModule && componentModule.component; + markAsModuleRoot(componentView, moduleNamePath); + return componentView; } const loadModule = profile("loadModule", (moduleNamePath: string, entry: ViewEntry): ModuleExports => { @@ -96,7 +98,7 @@ export const createViewFromEntry = profile("createViewFromEntry", (entry: ViewEn } else if (entry.moduleName) { // Current app full path. const currentAppPath = knownFolders.currentApp().path; - + // Full path of the module = current app full path + module name. const moduleNamePath = path.join(currentAppPath, entry.moduleName); const moduleExports = loadModule(moduleNamePath, entry); @@ -108,7 +110,7 @@ export const createViewFromEntry = profile("createViewFromEntry", (entry: ViewEn return viewFromBuilder(moduleNamePath, moduleExports); } } - + throw new Error("Failed to load page XML file for module: " + entry.moduleName); }); @@ -128,7 +130,7 @@ interface ModuleExports { const moduleCreateView = profile("module.createView", (moduleNamePath: string, moduleExports: ModuleExports): View => { const view = moduleExports.createPage(); const cssFileName = resolveFileName(moduleNamePath, "css"); - + // If there is no cssFile only appCss will be applied at loaded. if (cssFileName) { view.addCssFile(cssFileName); @@ -136,6 +138,12 @@ const moduleCreateView = profile("module.createView", (moduleNamePath: string, m return view; }); +function markAsModuleRoot(componentView: View, moduleNamePath: string): void { + const lastIndexOfSeparator = moduleNamePath.lastIndexOf(path.separator); + const moduleName = moduleNamePath.substring(lastIndexOfSeparator + 1); + componentView._moduleName = moduleName; +} + function loadInternal(fileName: string, context?: any, moduleNamePath?: string): ComponentModule { let componentModule: ComponentModule; diff --git a/tns-core-modules/ui/core/view-base/view-base.d.ts b/tns-core-modules/ui/core/view-base/view-base.d.ts index 0ef767f61..abda166de 100644 --- a/tns-core-modules/ui/core/view-base/view-base.d.ts +++ b/tns-core-modules/ui/core/view-base/view-base.d.ts @@ -108,6 +108,12 @@ export abstract class ViewBase extends Observable { flexWrapBefore: FlexWrapBefore; alignSelf: AlignSelf; + /** + * @private + * Module name when the view is a module root. Otherwise, it is undefined. + */ + _moduleName?: string; + //@private /** * @private diff --git a/tns-core-modules/ui/core/view-base/view-base.ts b/tns-core-modules/ui/core/view-base/view-base.ts index a09464c4a..34e8ff982 100644 --- a/tns-core-modules/ui/core/view-base/view-base.ts +++ b/tns-core-modules/ui/core/view-base/view-base.ts @@ -244,6 +244,8 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition public _defaultPaddingLeft: number; public _isPaddingRelative: boolean; + public _moduleName: string; + constructor() { super(); this._domId = viewIdCounter++; @@ -651,7 +653,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition } public resetNativeView(): void { - // + // } private resetNativeViewInternal(): void { @@ -688,7 +690,7 @@ export abstract class ViewBase extends Observable implements ViewBaseDefinition this._context = context; - // This will account for nativeView that is created in createNativeView, recycled + // This will account for nativeView that is created in createNativeView, recycled // or for backward compatability - set before _setupUI in iOS contructor. let nativeView = this.nativeViewProtected; diff --git a/tns-core-modules/ui/core/view/view-common.ts b/tns-core-modules/ui/core/view/view-common.ts index f12fabdc3..15409aa5c 100644 --- a/tns-core-modules/ui/core/view/view-common.ts +++ b/tns-core-modules/ui/core/view/view-common.ts @@ -5,7 +5,7 @@ import { } from "."; import { - ViewBase, Property, booleanConverter, EventData, layout, + ViewBase, Property, booleanConverter, eachDescendant, EventData, layout, getEventOrGestureName, traceEnabled, traceWrite, traceCategories, InheritedProperty, ShowModalOptions @@ -136,6 +136,37 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { } } + public _onLivesync(context?: ModuleContext): boolean { + _rootModalViews.forEach(v => v.closeModal()); + _rootModalViews.length = 0; + + // Currently, we pass `context` only for style modules + if (context && context.path) { + return this.changeLocalStyles(context.path); + } + + return false; + } + + private changeLocalStyles(contextPath: string): boolean { + if (!this.changeStyles(this, contextPath)) { + eachDescendant(this, (child: ViewBase) => { + this.changeStyles(child, contextPath); + return true; + }); + } + // Do not execute frame navigation for a change in styles + return true; + } + + private changeStyles(view: ViewBase, contextPath: string): boolean { + if (view._moduleName && contextPath.includes(view._moduleName)) { + (view).changeCssFile(contextPath); + return true; + } + return false; + } + _setupAsRootView(context: any): void { super._setupAsRootView(context); if (!this._styleScope) { @@ -210,12 +241,6 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { } } - _onLivesync(): boolean { - _rootModalViews.forEach(v => v.closeModal()); - _rootModalViews.length = 0; - return false; - } - public onBackPressed(): boolean { return false; } diff --git a/tns-core-modules/ui/frame/frame-common.ts b/tns-core-modules/ui/frame/frame-common.ts index c9f47b205..6c55fcdb2 100644 --- a/tns-core-modules/ui/frame/frame-common.ts +++ b/tns-core-modules/ui/frame/frame-common.ts @@ -564,44 +564,34 @@ export class FrameBase extends CustomLayoutView implements FrameDefinition { } public _onLivesync(context?: ModuleContext): boolean { - super._onLivesync(); - - if (!this._currentEntry || !this._currentEntry.entry) { - return false; - } - - const currentEntry = this._currentEntry.entry; - if (context && context.path) { - // Use topmost instead of this to cover nested frames scenario - const topmostFrame = topmost(); - const moduleName = topmostFrame.currentEntry.moduleName; - const reapplyStyles = context.path.includes(moduleName); - if (reapplyStyles && moduleName) { - topmostFrame.currentPage.changeCssFile(context.path); - return true; - } - } - - const newEntry: NavigationEntry = { - animated: false, - clearHistory: true, - context: currentEntry.context, - create: currentEntry.create, - moduleName: currentEntry.moduleName, - backstackVisible: currentEntry.backstackVisible - } - - // If create returns the same page instance we can't recreate it. - // Instead of navigation set activity content. - // This could happen if current page was set in XML as a Page instance. - if (newEntry.create) { - const page = newEntry.create(); - if (page === this.currentPage) { + // Execute a navigation if not handled on `View` level + if (!super._onLivesync(context)) { + if (!this._currentEntry || !this._currentEntry.entry) { return false; } - } - this.navigate(newEntry); + const currentEntry = this._currentEntry.entry; + const newEntry: NavigationEntry = { + animated: false, + clearHistory: true, + context: currentEntry.context, + create: currentEntry.create, + moduleName: currentEntry.moduleName, + backstackVisible: currentEntry.backstackVisible + } + + // If create returns the same page instance we can't recreate it. + // Instead of navigation set activity content. + // This could happen if current page was set in XML as a Page instance. + if (newEntry.create) { + const page = newEntry.create(); + if (page === this.currentPage) { + return false; + } + } + + this.navigate(newEntry); + } return true; } }