diff --git a/tests/app/app/app-new.css b/tests/app/app/app-new.css new file mode 100644 index 000000000..2b3afb687 --- /dev/null +++ b/tests/app/app/app-new.css @@ -0,0 +1,3 @@ +Button, Label { + color: green; +} diff --git a/tests/app/app/app-new.scss b/tests/app/app/app-new.scss new file mode 100644 index 000000000..2b3afb687 --- /dev/null +++ b/tests/app/app/app-new.scss @@ -0,0 +1,3 @@ +Button, Label { + color: green; +} diff --git a/tests/app/app/application.css b/tests/app/app/application.css new file mode 100644 index 000000000..119b358a7 --- /dev/null +++ b/tests/app/app/application.css @@ -0,0 +1,3 @@ +Button, Label { + color: black; +} diff --git a/tests/app/app/mainPage.ts b/tests/app/app/mainPage.ts index 70e2ce2e7..4c16fca02 100644 --- a/tests/app/app/mainPage.ts +++ b/tests/app/app/mainPage.ts @@ -2,6 +2,8 @@ import * as trace from "tns-core-modules/trace"; import * as tests from "../testRunner"; +let executeTests = true; + trace.enable(); trace.addCategories(trace.categories.Test + "," + trace.categories.Error); @@ -21,6 +23,8 @@ function runTests() { export function onNavigatedTo(args) { args.object.off(Page.loadedEvent, onNavigatedTo); - - runTests(); + if (executeTests) { + executeTests = false; + runTests(); + } } diff --git a/tests/app/app/mainPage.xml b/tests/app/app/mainPage.xml index bee9a71d8..7cead486d 100644 --- a/tests/app/app/mainPage.xml +++ b/tests/app/app/mainPage.xml @@ -1,3 +1,3 @@ - diff --git a/tests/app/livesync/livesync-tests.ts b/tests/app/livesync/livesync-tests.ts new file mode 100644 index 000000000..8ae772203 --- /dev/null +++ b/tests/app/livesync/livesync-tests.ts @@ -0,0 +1,113 @@ +import * as app from "tns-core-modules/application/application"; +import * as frame from "tns-core-modules/ui/frame"; +import * as helper from "../ui/helper"; +import * as TKUnit from "../TKUnit"; +import { Color } from "tns-core-modules/color"; +import { parse } from "tns-core-modules/ui/builder"; +import { Page } from "tns-core-modules/ui/page"; + +const appCssFileName = "./app/application.css"; +const appNewCssFileName = "./app/app-new.css"; +const appNewScssFileName = "./app/app-new.scss"; +const appJsFileName = "./app/app.js"; +const appTsFileName = "./app/app.ts"; +const mainPageCssFileName = "./app/main-page.css"; +const mainPageHtmlFileName = "./app/main-page.html"; +const mainPageXmlFileName = "./app/main-page.xml"; + +const green = new Color("green"); + +const mainPageTemplate = ` + + + + + `; + +const pageTemplate = ` + + + + + `; + +export function test_onLiveSync_HmrContext_AppStyle_AppNewCss() { + _test_onLiveSync_HmrContext_AppStyle(appNewCssFileName); +} + +export function test_onLiveSync_HmrContext_AppStyle_AppNewScss() { + _test_onLiveSync_HmrContext_AppStyle(appNewScssFileName); +} + +export function test_onLiveSync_HmrContext_ContextUndefined() { + _test_onLiveSync_HmrContext({ type: undefined, module: undefined }); +} + +export function test_onLiveSync_HmrContext_ModuleUndefined() { + _test_onLiveSync_HmrContext({ type: "script", module: undefined }); +} + +export function test_onLiveSync_HmrContext_Script_AppJs() { + _test_onLiveSync_HmrContext({ type: "script", module: appJsFileName }); +} + +export function test_onLiveSync_HmrContext_Script_AppTs() { + _test_onLiveSync_HmrContext({ type: "script", module: appTsFileName }); +} + +export function test_onLiveSync_HmrContext_Style_MainPageCss() { + _test_onLiveSync_HmrContext({ type: "style", module: mainPageCssFileName }); +} + +export function test_onLiveSync_HmrContext_Markup_MainPageHtml() { + _test_onLiveSync_HmrContext({ type: "markup", module: mainPageHtmlFileName }); +} + +export function test_onLiveSync_HmrContext_Markup_MainPageXml() { + _test_onLiveSync_HmrContext({ type: "markup", module: mainPageXmlFileName }); +} + +export function setUpModule() { + const mainPage = parse(mainPageTemplate); + helper.navigate(() => mainPage); +} + +export function tearDown() { + app.setCssFileName(appCssFileName); +} + +function _test_onLiveSync_HmrContext_AppStyle(styleFileName: string) { + const pageBeforeNavigation = helper.getCurrentPage(); + + const page = parse(pageTemplate); + helper.navigateWithHistory(() => page); + app.setCssFileName(styleFileName); + + const pageBeforeLiveSync = helper.getCurrentPage(); + global.__onLiveSync({ type: "style", module: styleFileName }); + + const pageAfterLiveSync = helper.getCurrentPage(); + TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString()); + + TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "App 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, green, "App styles NOT 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!"); +} + +function _test_onLiveSync_HmrContext(context: { type, module }) { + const page = parse(pageTemplate); + helper.navigateWithHistory(() => page); + global.__onLiveSync({ type: context.type, module: context.module }); + + TKUnit.waitUntilReady(() => !!frame.topmost()); + const topmostFrame = frame.topmost(); + TKUnit.waitUntilReady(() => topmostFrame.currentPage && topmostFrame.currentPage.isLoaded && !topmostFrame.canGoBack()); + TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded); +} \ No newline at end of file diff --git a/tests/app/testRunner.ts b/tests/app/testRunner.ts index a119642a0..b96bde95d 100644 --- a/tests/app/testRunner.ts +++ b/tests/app/testRunner.ts @@ -153,9 +153,6 @@ allTests["STYLE-PROPERTIES"] = stylePropertiesTests; import * as frameTests from "./ui/frame/frame-tests"; allTests["FRAME"] = frameTests; -import * as tabViewRootTests from "./ui/tab-view/tab-view-root-tests"; -allTests["TAB-VIEW-ROOT"] = tabViewRootTests; - import * as viewTests from "./ui/view/view-tests"; allTests["VIEW"] = viewTests; @@ -255,6 +252,12 @@ allTests["SEARCH-BAR"] = searchBarTests; import * as navigationTests from "./navigation/navigation-tests"; allTests["NAVIGATION"] = navigationTests; +import * as livesyncTests from "./livesync/livesync-tests"; +allTests["LIVESYNC"] = livesyncTests; + +import * as tabViewRootTests from "./ui/tab-view/tab-view-root-tests"; +allTests["TAB-VIEW-ROOT"] = tabViewRootTests; + import * as resetRootViewTests from "./ui/root-view/reset-root-view-tests"; allTests["RESET-ROOT-VIEW"] = resetRootViewTests; diff --git a/tns-core-modules/application/application-common.ts b/tns-core-modules/application/application-common.ts index 32ab9dc04..3f0d0888c 100644 --- a/tns-core-modules/application/application-common.ts +++ b/tns-core-modules/application/application-common.ts @@ -32,7 +32,14 @@ export function hasLaunched(): boolean { export { Observable }; -import { UnhandledErrorEventData, iOSApplication, AndroidApplication, CssChangedEventData, LoadAppCSSEventData } from "."; +import { + AndroidApplication, + CssChangedEventData, + getRootView, + iOSApplication, + LoadAppCSSEventData, + UnhandledErrorEventData +} from "./application"; export { UnhandledErrorEventData, CssChangedEventData, LoadAppCSSEventData }; @@ -70,10 +77,21 @@ export function setApplication(instance: iOSApplication | AndroidApplication): v app = instance; } -export function livesync() { +export function livesync(context?: HmrContext) { events.notify({ eventName: "livesync", object: app }); const liveSyncCore = global.__onLiveSyncCore; - if (liveSyncCore) { + let reapplyAppCss = false + + if (context) { + const fullFileName = getCssFileName(); + const fileName = fullFileName.substring(0, fullFileName.lastIndexOf(".") + 1); + const extensions = ["css", "scss"]; + reapplyAppCss = extensions.some(ext => context.module === fileName.concat(ext)); + } + + if (reapplyAppCss) { + getRootView()._onCssStateChange(); + } else if (liveSyncCore) { liveSyncCore(); } } @@ -92,7 +110,7 @@ export function loadAppCss(): void { events.notify({ eventName: "loadAppCss", object: app, cssFile: getCssFileName() }); } catch (e) { throw new Error(`The file ${getCssFileName()} couldn't be loaded! ` + - `You may need to register it inside ./app/vendor.ts.`); + `You may need to register it inside ./app/vendor.ts.`); } } diff --git a/tns-core-modules/application/application.android.ts b/tns-core-modules/application/application.android.ts index 0b33bba80..6d5166675 100644 --- a/tns-core-modules/application/application.android.ts +++ b/tns-core-modules/application/application.android.ts @@ -212,12 +212,12 @@ export function getNativeApplication(): android.app.Application { return nativeApp; } -global.__onLiveSync = function () { +global.__onLiveSync = function __onLiveSync(context?: HmrContext) { if (androidApp && androidApp.paused) { return; } - livesync(); + livesync(context); }; function initLifecycleCallbacks() { diff --git a/tns-core-modules/application/application.ios.ts b/tns-core-modules/application/application.ios.ts index 867f2ef6f..af6028cd4 100644 --- a/tns-core-modules/application/application.ios.ts +++ b/tns-core-modules/application/application.ios.ts @@ -161,7 +161,7 @@ class IOSApplication implements IOSApplicationDefinition { this.setWindowContent(args.root); } else { this._window = UIApplication.sharedApplication.delegate.window; - } + } } @profile @@ -373,10 +373,10 @@ function setViewControllerView(view: View): void { } } -global.__onLiveSync = function () { +global.__onLiveSync = function __onLiveSync(context?: HmrContext) { if (!started) { return; } - livesync(); -} + livesync(context); +} \ No newline at end of file diff --git a/tns-core-modules/module.d.ts b/tns-core-modules/module.d.ts index 5fd5d161d..0a0f6a033 100644 --- a/tns-core-modules/module.d.ts +++ b/tns-core-modules/module.d.ts @@ -51,7 +51,7 @@ declare namespace NodeJS { __native?: any; __inspector?: any; __extends: any; - __onLiveSync: () => void; + __onLiveSync: (context?: { type: string, module: string }) => void; __onLiveSyncCore: () => void; __onUncaughtError: (error: NativeScriptError) => void; TNS_WEBPACK?: boolean; @@ -64,6 +64,27 @@ declare function clearTimeout(timeoutId: number): void; declare function setInterval(callback: (...args: any[]) => void, ms: number, ...args: any[]): number; declare function clearInterval(intervalId: number): void; +declare enum HmrType { + markup = "markup", + script = "script", + style = "style" +} + +/** + * Define a context for Hot Module Replacement. + */ +interface HmrContext { + /** + * The type of module for replacement. + */ + type: HmrType; + + /** + * The module for replacement. + */ + module: string; +} + /** * An extended JavaScript Error which will have the nativeError property initialized in case the error is caused by executing platform-specific code. */ diff --git a/tns-core-modules/ui/frame/frame.android.ts b/tns-core-modules/ui/frame/frame.android.ts index 1f56a8eb2..6756bf915 100644 --- a/tns-core-modules/ui/frame/frame.android.ts +++ b/tns-core-modules/ui/frame/frame.android.ts @@ -1198,4 +1198,4 @@ export function setActivityCallbacks(activity: android.support.v7.app.AppCompatA export function setFragmentCallbacks(fragment: android.support.v4.app.Fragment): void { fragment[CALLBACKS] = new FragmentCallbacksImplementation(); -} +} \ No newline at end of file diff --git a/tns-core-modules/ui/page/page-common.ts b/tns-core-modules/ui/page/page-common.ts index 0d85ab1ab..28814e186 100644 --- a/tns-core-modules/ui/page/page-common.ts +++ b/tns-core-modules/ui/page/page-common.ts @@ -17,17 +17,17 @@ export class PageBase extends ContentView implements PageDefinition { public static navigatedToEvent = "navigatedTo"; public static navigatingFromEvent = "navigatingFrom"; public static navigatedFromEvent = "navigatedFrom"; - + private _navigationContext: any; private _actionBar: ActionBar; public _frame: Frame; - + public actionBarHidden: boolean; public enableSwipeBackNavigation: boolean; public backgroundSpanUnderStatusBar: boolean; public hasActionBar: boolean; - + get navigationContext(): any { return this._navigationContext; } @@ -89,7 +89,7 @@ export class PageBase extends ContentView implements PageDefinition { const frame = this.parent; return frame instanceof Frame ? frame : undefined; } - + private createNavigatedData(eventName: string, isBackNavigation: boolean): NavigatedData { return { eventName: eventName, @@ -103,6 +103,13 @@ export class PageBase extends ContentView implements PageDefinition { public onNavigatingTo(context: any, isBackNavigation: boolean, bindingContext?: any) { this._navigationContext = context; + if (isBackNavigation && this._styleScope) { + this._styleScope.ensureSelectors(); + if (!this._cssState.isSelectorsLatestVersionApplied()) { + this._onCssStateChange(); + } + } + //https://github.com/NativeScript/NativeScript/issues/731 if (!isBackNavigation && bindingContext !== undefined && bindingContext !== null) { this.bindingContext = bindingContext; diff --git a/tns-core-modules/ui/styling/style-scope.d.ts b/tns-core-modules/ui/styling/style-scope.d.ts index b404ce9a7..b5bf01684 100644 --- a/tns-core-modules/ui/styling/style-scope.d.ts +++ b/tns-core-modules/ui/styling/style-scope.d.ts @@ -19,6 +19,11 @@ export class CssState { * Gets the static selectors that match the view and the dynamic selectors that may potentially match the view. */ public changeMap: ChangeMap; + + /** + * Checks whether style scope and CSS state selectors are in sync. + */ + public isSelectorsLatestVersionApplied(): boolean } export class StyleScope { @@ -29,6 +34,9 @@ export class StyleScope { public static createSelectorsFromImports(tree: SyntaxTree, keyframes: Object): RuleSet[]; public ensureSelectors(): number; + public isApplicationCssSelectorsLatestVersionApplied(): boolean; + public isLocalCssSelectorsLatestVersionApplied(): boolean; + public applySelectors(view: ViewBase): void public query(options: Node): SelectorCore[]; diff --git a/tns-core-modules/ui/styling/style-scope.ts b/tns-core-modules/ui/styling/style-scope.ts index 8f3cf607c..a915b559d 100644 --- a/tns-core-modules/ui/styling/style-scope.ts +++ b/tns-core-modules/ui/styling/style-scope.ts @@ -271,7 +271,7 @@ export function removeTaggedAdditionalCSS(tag: String | Number): Boolean { changed = true; } } - if (changed) { mergeCssSelectors(); } + if (changed) { mergeCssSelectors(); } return changed; } @@ -343,6 +343,7 @@ export class CssState { _appliedChangeMap: Readonly>; _appliedPropertyValues: Readonly<{}>; _appliedAnimations: ReadonlyArray; + _appliedSelectorsVersion: number; _match: SelectorsMatch; _matchInvalid: boolean; @@ -367,6 +368,10 @@ export class CssState { } } + public isSelectorsLatestVersionApplied(): boolean { + return this.view._styleScope._getSelectorsVersion() === this._appliedSelectorsVersion; + } + public onLoaded(): void { if (this._matchInvalid) { this.updateMatch(); @@ -381,7 +386,12 @@ export class CssState { @profile private updateMatch() { - this._match = this.view._styleScope ? this.view._styleScope.matchSelectors(this.view) : CssState.emptyMatch; + if (this.view._styleScope) { + this._appliedSelectorsVersion = this.view._styleScope._getSelectorsVersion(); + this._match = this.view._styleScope.matchSelectors(this.view); + } else { + this._match = CssState.emptyMatch; + } this._matchInvalid = false; } @@ -597,8 +607,8 @@ export class StyleScope { } public ensureSelectors(): number { - if (this._applicationCssSelectorsAppliedVersion !== applicationCssSelectorVersion || - this._localCssSelectorVersion !== this._localCssSelectorsAppliedVersion || + if (!this.isApplicationCssSelectorsLatestVersionApplied() || + !this.isLocalCssSelectorsLatestVersionApplied() || !this._mergedCssSelectors) { this._createSelectors(); @@ -607,6 +617,14 @@ export class StyleScope { return this._getSelectorsVersion(); } + public isApplicationCssSelectorsLatestVersionApplied(): boolean { + return this._applicationCssSelectorsAppliedVersion === applicationCssSelectorVersion; + } + + public isLocalCssSelectorsLatestVersionApplied(): boolean { + return this._localCssSelectorsAppliedVersion === this._localCssSelectorVersion; + } + @profile private _createSelectors() { let toMerge: RuleSet[][] = [];