Skip to content

Commit

Permalink
feat(hmr): preserve navigation history on applying changes (#7146)
Browse files Browse the repository at this point in the history
  • Loading branch information
vchimev authored Apr 23, 2019
1 parent 4e56c89 commit d35e14e
Show file tree
Hide file tree
Showing 23 changed files with 413 additions and 167 deletions.
File renamed without changes.
5 changes: 5 additions & 0 deletions tests/app/app/button-page.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Page loaded="onLoaded">
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>
3 changes: 3 additions & 0 deletions tests/app/livesync/livesync-button-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function onLoaded() {
console.log("Button page loaded!");
}
5 changes: 5 additions & 0 deletions tests/app/livesync/livesync-button-page.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Page loaded="onLoaded">
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>
3 changes: 3 additions & 0 deletions tests/app/livesync/livesync-label-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function onLoaded() {
console.log("Label page loaded!");
}
5 changes: 5 additions & 0 deletions tests/app/livesync/livesync-label-page.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Page loaded="onLoaded">
<StackLayout>
<Label id="label" text="label"></Label>
</StackLayout>
</Page>
133 changes: 74 additions & 59 deletions tests/app/livesync/livesync-tests.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
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 * as app from "tns-core-modules/application/application";
import * as frame from "tns-core-modules/ui/frame";

import { Color } from "tns-core-modules/color";
import { parse } from "tns-core-modules/ui/builder";
import { isAndroid } from "tns-core-modules/platform";
import { createViewFromEntry } from "tns-core-modules/ui/builder";
import { Page } from "tns-core-modules/ui/page";
import { Frame } from "tns-core-modules/ui/frame";

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 black = new Color("black");
const green = new Color("green");
const buttonCssFileName = "./app/button-page.css";

const mainPageTemplate = `
<Page>
<StackLayout>
<Label id="label" text="label"></Label>
</StackLayout>
</Page>`;
const buttonPageModuleName = "livesync/livesync-button-page";
const buttonHtmlPageFileName = "./livesync/livesync-button-page.html";
const buttonXmlPageFileName = "./livesync/livesync-button-page.xml";
const buttonJsPageFileName = "./livesync/livesync-button-page.js";
const buttonTsPageFileName = "./livesync/livesync-button-page.ts";
const labelPageModuleName = "livesync/livesync-label-page";

const pageTemplate = `
<Page>
<StackLayout>
<Button id="button" text="button"></Button>
</StackLayout>
</Page>`;
const green = new Color("green");

export function test_onLiveSync_ModuleContext_AppStyle_AppNewCss() {
_test_onLiveSync_ModuleContext_AppStyle(appNewCssFileName);
Expand All @@ -48,29 +40,29 @@ export function test_onLiveSync_ModuleContext_ModuleUndefined() {
_test_onLiveSync_ModuleContext({ type: "script", path: undefined });
}

export function test_onLiveSync_ModuleContext_Script_AppJs() {
_test_onLiveSync_ModuleContext({ type: "script", path: appJsFileName });
export function test_onLiveSync_ModuleContext_Script_JsFile() {
_test_onLiveSync_ModuleReplace({ type: "script", path: buttonJsPageFileName });
}

export function test_onLiveSync_ModuleContext_Script_AppTs() {
_test_onLiveSync_ModuleContext({ type: "script", path: appTsFileName });
export function test_onLiveSync_ModuleContext_Script_TsFile() {
_test_onLiveSync_ModuleReplace({ type: "script", path: buttonTsPageFileName });
}

export function test_onLiveSync_ModuleContext_Style_MainPageCss() {
_test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName });
export function test_onLiveSync_ModuleContext_Style_CssFile() {
_test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: buttonCssFileName });
}

export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() {
_test_onLiveSync_ModuleContext({ type: "markup", path: mainPageHtmlFileName });
export function test_onLiveSync_ModuleContext_Markup_HtmlFile() {
_test_onLiveSync_ModuleReplace({ type: "markup", path: buttonHtmlPageFileName });
}

export function test_onLiveSync_ModuleContext_Markup_MainPageXml() {
_test_onLiveSync_ModuleContext({ type: "markup", path: mainPageXmlFileName });
export function test_onLiveSync_ModuleContext_Markup_XmlFile() {
_test_onLiveSync_ModuleReplace({ type: "markup", path: buttonXmlPageFileName });
}

export function setUpModule() {
const mainPage = <Page>parse(mainPageTemplate);
helper.navigate(() => mainPage);
export function setUp() {
const labelPage = <Page>createViewFromEntry(({ moduleName: labelPageModuleName }));
helper.navigate(() => labelPage);
}

export function tearDown() {
Expand All @@ -79,32 +71,29 @@ export function tearDown() {

function _test_onLiveSync_ModuleContext_AppStyle(styleFileName: string) {
const pageBeforeNavigation = helper.getCurrentPage();
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
helper.navigateWithHistory(() => buttonPage);

const page = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
app.setCssFileName(styleFileName);

const pageBeforeLiveSync = helper.getCurrentPage();
global.__onLiveSync({ type: "style", path: 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!");
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!");
TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different!");
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is 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.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different");
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
}

function _test_onLiveSync_ModuleContext(context: { type, path }) {
const page = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
helper.navigateWithHistory(() => buttonPage);
global.__onLiveSync({ type: context.type, path: context.path });

TKUnit.waitUntilReady(() => !!frame.topmost());
Expand All @@ -113,27 +102,53 @@ function _test_onLiveSync_ModuleContext(context: { type, path }) {
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded);
}

function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
function _test_onLiveSync_ModuleReplace(context: { type, path }) {
const pageBeforeNavigation = helper.getCurrentPage();
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
helper.navigateWithHistory(() => buttonPage);

global.__onLiveSync({ type: context.type, path: context.path });
const topmostFrame = frame.topmost();
waitUntilLivesyncComplete(topmostFrame);
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("button").isLoaded, "Button page is NOT loaded!");
TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!");
TKUnit.assertTrue(topmostFrame.canGoBack(), "Can NOT go back!");

const page = <Page>parse(pageTemplate);
helper.navigateWithHistory(() => page);
helper.goBack();
const pageAfterBackNavigation = helper.getCurrentPage();
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded, "Label page is NOT loaded!");
TKUnit.assertEqual(topmostFrame.backStack.length, 0, "Backstack is NOT clean!");
TKUnit.assertEqual(pageBeforeNavigation, pageAfterBackNavigation, "Pages are different!");
}

function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
const pageBeforeNavigation = helper.getCurrentPage();
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
helper.navigateWithHistory(() => buttonPage);

const pageBeforeLiveSync = helper.getCurrentPage();
pageBeforeLiveSync._moduleName = "main-page";
pageBeforeLiveSync._moduleName = "button-page";

global.__onLiveSync({ type: context.type, path: context.path });
const topmostFrame = frame.topmost();
waitUntilLivesyncComplete(topmostFrame);

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!");
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!");
TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!");
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is 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.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different!");
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
}
}

function waitUntilLivesyncComplete(frame: Frame) {
if (isAndroid) {
TKUnit.waitUntilReady(() => frame._executingEntry === null);
} else {
TKUnit.waitUntilReady(() => frame.currentPage.isLoaded);
}
}
8 changes: 4 additions & 4 deletions tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"repository": "<fill-your-repository-here>",
"nativescript": {
"id": "org.nativescript.UnitTestApp",
"tns-ios": {
"version": "5.2.0"
},
"tns-android": {
"version": "5.2.1"
"version": "5.3.1"
},
"tns-ios": {
"version": "5.3.1"
}
},
"dependencies": {
Expand Down
12 changes: 5 additions & 7 deletions tns-core-modules/application/application-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,22 +83,20 @@ export function livesync(rootView: View, context?: ModuleContext) {
events.notify(<EventData>{ eventName: "livesync", object: app });
const liveSyncCore = global.__onLiveSyncCore;
let reapplyAppStyles = false;
let reapplyLocalStyles = false;

// ModuleContext is available only for Hot Module Replacement
if (context && context.path) {
const extensions = ["css", "scss"];
const styleExtensions = ["css", "scss"];
const appStylesFullFileName = getCssFileName();
const appStylesFileName = appStylesFullFileName.substring(0, appStylesFullFileName.lastIndexOf(".") + 1);
reapplyAppStyles = extensions.some(ext => context.path === appStylesFileName.concat(ext));
if (!reapplyAppStyles) {
reapplyLocalStyles = extensions.some(ext => context.path.endsWith(ext));
}
reapplyAppStyles = styleExtensions.some(ext => context.path === appStylesFileName.concat(ext));
}

// Handle application styles
if (reapplyAppStyles && rootView) {
rootView._onCssStateChange();
} else if (liveSyncCore) {
reapplyLocalStyles ? liveSyncCore(context) : liveSyncCore();
liveSyncCore(context);
}
}

Expand Down
15 changes: 12 additions & 3 deletions tns-core-modules/application/application.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
notify, launchEvent, resumeEvent, suspendEvent, exitEvent, lowMemoryEvent,
orientationChangedEvent, setApplication, livesync, displayedEvent, getCssFileName
} from "./application-common";
import { ModuleType } from "../ui/core/view/view-common";

// First reexport so that app module is initialized.
export * from "./application-common";
Expand Down Expand Up @@ -106,6 +107,7 @@ class IOSApplication implements IOSApplicationDefinition {
get delegate(): typeof UIApplicationDelegate {
return this._delegate;
}

set delegate(value: typeof UIApplicationDelegate) {
if (this._delegate !== value) {
this._delegate = value;
Expand Down Expand Up @@ -228,8 +230,16 @@ class IOSApplication implements IOSApplicationDefinition {
}

public _onLivesync(context?: ModuleContext): void {
// If view can't handle livesync set window controller.
if (this._rootView && !this._rootView._onLivesync(context)) {
// Handle application root module
const isAppRootModuleChanged = context && context.path && context.path.includes(getMainEntry().moduleName) && context.type !== ModuleType.style;

// Set window content when:
// + Application root module is changed
// + View did not handle the change
// Note:
// The case when neither app root module is changed, nor livesync is handled on View,
// then changes will not apply until navigate forward to the module.
if (isAppRootModuleChanged || (this._rootView && !this._rootView._onLivesync(context))) {
this.setWindowContent();
}
}
Expand Down Expand Up @@ -258,7 +268,6 @@ class IOSApplication implements IOSApplicationDefinition {
this._window.makeKeyAndVisible();
}
}

}

const iosApp = new IOSApplication();
Expand Down
9 changes: 5 additions & 4 deletions tns-core-modules/trace/trace.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,11 @@ export module categories {
export const Error: string;
export const Animation: string;
export const Transition: string;
export const Livesync: string;

export const separator: string;
export const All: string;

export const separator: string;
export function concat(...categories: string[]): string;
}

Expand All @@ -125,20 +126,20 @@ export interface TraceWriter {
}

/**
* An interface used to trace information about specific event.
* An interface used to trace information about specific event.
*/
export interface EventListener {
filter: string;
on(object: Object, name: string, data?: any);
}

/**
* An interface used to for handling trace error
* An interface used to for handling trace error
*/
export interface ErrorHandler {
handlerError(error: Error);
}

export class DefaultErrorHandler implements ErrorHandler {
handlerError(error);
}
}
15 changes: 14 additions & 1 deletion tns-core-modules/trace/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,22 @@ export module categories {
export const Error = "Error";
export const Animation = "Animation";
export const Transition = "Transition";
export const All = VisualTreeEvents + "," + Layout + "," + Style + "," + ViewHierarchy + "," + NativeLifecycle + "," + Debug + "," + Navigation + "," + Test + "," + Binding + "," + Error + "," + Animation + "," + Transition;
export const Livesync = "Livesync";

export const separator = ",";
export const All = VisualTreeEvents + separator
+ Layout + separator
+ Style + separator
+ ViewHierarchy + separator
+ NativeLifecycle + separator
+ Debug + separator
+ Navigation + separator
+ Test + separator
+ Binding + separator
+ Error + separator
+ Animation + separator
+ Transition + separator
+ Livesync;

export function concat(): string {
let result: string;
Expand Down
Loading

0 comments on commit d35e14e

Please sign in to comment.