Skip to content

Commit d35e14e

Browse files
authored
feat(hmr): preserve navigation history on applying changes (#7146)
1 parent 4e56c89 commit d35e14e

23 files changed

+413
-167
lines changed
File renamed without changes.

tests/app/app/button-page.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Page loaded="onLoaded">
2+
<StackLayout>
3+
<Button id="button" text="button"></Button>
4+
</StackLayout>
5+
</Page>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function onLoaded() {
2+
console.log("Button page loaded!");
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Page loaded="onLoaded">
2+
<StackLayout>
3+
<Button id="button" text="button"></Button>
4+
</StackLayout>
5+
</Page>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function onLoaded() {
2+
console.log("Label page loaded!");
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Page loaded="onLoaded">
2+
<StackLayout>
3+
<Label id="label" text="label"></Label>
4+
</StackLayout>
5+
</Page>

tests/app/livesync/livesync-tests.ts

Lines changed: 74 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,28 @@
1-
import * as app from "tns-core-modules/application/application";
2-
import * as frame from "tns-core-modules/ui/frame";
31
import * as helper from "../ui/helper";
42
import * as TKUnit from "../TKUnit";
3+
4+
import * as app from "tns-core-modules/application/application";
5+
import * as frame from "tns-core-modules/ui/frame";
6+
57
import { Color } from "tns-core-modules/color";
6-
import { parse } from "tns-core-modules/ui/builder";
8+
import { isAndroid } from "tns-core-modules/platform";
9+
import { createViewFromEntry } from "tns-core-modules/ui/builder";
710
import { Page } from "tns-core-modules/ui/page";
11+
import { Frame } from "tns-core-modules/ui/frame";
812

913
const appCssFileName = "./app/application.css";
1014
const appNewCssFileName = "./app/app-new.css";
1115
const appNewScssFileName = "./app/app-new.scss";
12-
const appJsFileName = "./app/app.js";
13-
const appTsFileName = "./app/app.ts";
14-
const mainPageCssFileName = "./app/main-page.css";
15-
const mainPageHtmlFileName = "./app/main-page.html";
16-
const mainPageXmlFileName = "./app/main-page.xml";
17-
18-
const black = new Color("black");
19-
const green = new Color("green");
16+
const buttonCssFileName = "./app/button-page.css";
2017

21-
const mainPageTemplate = `
22-
<Page>
23-
<StackLayout>
24-
<Label id="label" text="label"></Label>
25-
</StackLayout>
26-
</Page>`;
18+
const buttonPageModuleName = "livesync/livesync-button-page";
19+
const buttonHtmlPageFileName = "./livesync/livesync-button-page.html";
20+
const buttonXmlPageFileName = "./livesync/livesync-button-page.xml";
21+
const buttonJsPageFileName = "./livesync/livesync-button-page.js";
22+
const buttonTsPageFileName = "./livesync/livesync-button-page.ts";
23+
const labelPageModuleName = "livesync/livesync-label-page";
2724

28-
const pageTemplate = `
29-
<Page>
30-
<StackLayout>
31-
<Button id="button" text="button"></Button>
32-
</StackLayout>
33-
</Page>`;
25+
const green = new Color("green");
3426

3527
export function test_onLiveSync_ModuleContext_AppStyle_AppNewCss() {
3628
_test_onLiveSync_ModuleContext_AppStyle(appNewCssFileName);
@@ -48,29 +40,29 @@ export function test_onLiveSync_ModuleContext_ModuleUndefined() {
4840
_test_onLiveSync_ModuleContext({ type: "script", path: undefined });
4941
}
5042

51-
export function test_onLiveSync_ModuleContext_Script_AppJs() {
52-
_test_onLiveSync_ModuleContext({ type: "script", path: appJsFileName });
43+
export function test_onLiveSync_ModuleContext_Script_JsFile() {
44+
_test_onLiveSync_ModuleReplace({ type: "script", path: buttonJsPageFileName });
5345
}
5446

55-
export function test_onLiveSync_ModuleContext_Script_AppTs() {
56-
_test_onLiveSync_ModuleContext({ type: "script", path: appTsFileName });
47+
export function test_onLiveSync_ModuleContext_Script_TsFile() {
48+
_test_onLiveSync_ModuleReplace({ type: "script", path: buttonTsPageFileName });
5749
}
5850

59-
export function test_onLiveSync_ModuleContext_Style_MainPageCss() {
60-
_test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: mainPageCssFileName });
51+
export function test_onLiveSync_ModuleContext_Style_CssFile() {
52+
_test_onLiveSync_ModuleContext_TypeStyle({ type: "style", path: buttonCssFileName });
6153
}
6254

63-
export function test_onLiveSync_ModuleContext_Markup_MainPageHtml() {
64-
_test_onLiveSync_ModuleContext({ type: "markup", path: mainPageHtmlFileName });
55+
export function test_onLiveSync_ModuleContext_Markup_HtmlFile() {
56+
_test_onLiveSync_ModuleReplace({ type: "markup", path: buttonHtmlPageFileName });
6557
}
6658

67-
export function test_onLiveSync_ModuleContext_Markup_MainPageXml() {
68-
_test_onLiveSync_ModuleContext({ type: "markup", path: mainPageXmlFileName });
59+
export function test_onLiveSync_ModuleContext_Markup_XmlFile() {
60+
_test_onLiveSync_ModuleReplace({ type: "markup", path: buttonXmlPageFileName });
6961
}
7062

71-
export function setUpModule() {
72-
const mainPage = <Page>parse(mainPageTemplate);
73-
helper.navigate(() => mainPage);
63+
export function setUp() {
64+
const labelPage = <Page>createViewFromEntry(({ moduleName: labelPageModuleName }));
65+
helper.navigate(() => labelPage);
7466
}
7567

7668
export function tearDown() {
@@ -79,32 +71,29 @@ export function tearDown() {
7971

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

83-
const page = <Page>parse(pageTemplate);
84-
helper.navigateWithHistory(() => page);
8577
app.setCssFileName(styleFileName);
86-
8778
const pageBeforeLiveSync = helper.getCurrentPage();
8879
global.__onLiveSync({ type: "style", path: styleFileName });
8980

9081
const pageAfterLiveSync = helper.getCurrentPage();
9182
TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString());
92-
93-
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "App styles NOT applied - livesync navigation executed!");
94-
TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!");
95-
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!");
83+
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!");
84+
TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different!");
85+
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
9686

9787
helper.goBack();
98-
9988
const pageAfterNavigationBack = helper.getCurrentPage();
10089
TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, green, "App styles NOT applied on back navigation!");
101-
TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!");
90+
TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different");
10291
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
10392
}
10493

10594
function _test_onLiveSync_ModuleContext(context: { type, path }) {
106-
const page = <Page>parse(pageTemplate);
107-
helper.navigateWithHistory(() => page);
95+
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
96+
helper.navigateWithHistory(() => buttonPage);
10897
global.__onLiveSync({ type: context.type, path: context.path });
10998

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

116-
function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
105+
function _test_onLiveSync_ModuleReplace(context: { type, path }) {
117106
const pageBeforeNavigation = helper.getCurrentPage();
107+
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
108+
helper.navigateWithHistory(() => buttonPage);
109+
110+
global.__onLiveSync({ type: context.type, path: context.path });
111+
const topmostFrame = frame.topmost();
112+
waitUntilLivesyncComplete(topmostFrame);
113+
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("button").isLoaded, "Button page is NOT loaded!");
114+
TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!");
115+
TKUnit.assertTrue(topmostFrame.canGoBack(), "Can NOT go back!");
118116

119-
const page = <Page>parse(pageTemplate);
120-
helper.navigateWithHistory(() => page);
117+
helper.goBack();
118+
const pageAfterBackNavigation = helper.getCurrentPage();
119+
TKUnit.assertTrue(topmostFrame.currentPage.getViewById("label").isLoaded, "Label page is NOT loaded!");
120+
TKUnit.assertEqual(topmostFrame.backStack.length, 0, "Backstack is NOT clean!");
121+
TKUnit.assertEqual(pageBeforeNavigation, pageAfterBackNavigation, "Pages are different!");
122+
}
123+
124+
function _test_onLiveSync_ModuleContext_TypeStyle(context: { type, path }) {
125+
const pageBeforeNavigation = helper.getCurrentPage();
126+
const buttonPage = <Page>createViewFromEntry(({ moduleName: buttonPageModuleName }));
127+
helper.navigateWithHistory(() => buttonPage);
121128

122129
const pageBeforeLiveSync = helper.getCurrentPage();
123-
pageBeforeLiveSync._moduleName = "main-page";
130+
pageBeforeLiveSync._moduleName = "button-page";
131+
124132
global.__onLiveSync({ type: context.type, path: context.path });
133+
const topmostFrame = frame.topmost();
134+
waitUntilLivesyncComplete(topmostFrame);
125135

126136
const pageAfterLiveSync = helper.getCurrentPage();
127137
TKUnit.waitUntilReady(() => pageAfterLiveSync.getViewById("button").style.color.toString() === green.toString());
128-
129-
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Local styles NOT applied - livesync navigation executed!");
130-
TKUnit.assertEqual(pageAfterLiveSync, pageBeforeLiveSync, "Pages are different - livesync navigation executed!");
131-
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version NOT applied!");
138+
TKUnit.assertTrue(pageAfterLiveSync.frame.canGoBack(), "Can NOT go back!");
139+
TKUnit.assertEqual(topmostFrame.backStack.length, 1, "Backstack is clean!");
140+
TKUnit.assertTrue(pageAfterLiveSync._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
132141

133142
helper.goBack();
134-
135143
const pageAfterNavigationBack = helper.getCurrentPage();
136-
TKUnit.assertEqual(pageAfterNavigationBack.getViewById("label").style.color, black, "App styles applied on back navigation!");
137-
TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different - livesync navigation executed!");
144+
TKUnit.assertEqual(pageBeforeNavigation, pageAfterNavigationBack, "Pages are different!");
138145
TKUnit.assertTrue(pageAfterNavigationBack._cssState.isSelectorsLatestVersionApplied(), "Latest selectors version is NOT applied!");
139-
}
146+
}
147+
148+
function waitUntilLivesyncComplete(frame: Frame) {
149+
if (isAndroid) {
150+
TKUnit.waitUntilReady(() => frame._executingEntry === null);
151+
} else {
152+
TKUnit.waitUntilReady(() => frame.currentPage.isLoaded);
153+
}
154+
}

tests/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
"repository": "<fill-your-repository-here>",
66
"nativescript": {
77
"id": "org.nativescript.UnitTestApp",
8-
"tns-ios": {
9-
"version": "5.2.0"
10-
},
118
"tns-android": {
12-
"version": "5.2.1"
9+
"version": "5.3.1"
10+
},
11+
"tns-ios": {
12+
"version": "5.3.1"
1313
}
1414
},
1515
"dependencies": {

tns-core-modules/application/application-common.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,20 @@ export function livesync(rootView: View, context?: ModuleContext) {
8383
events.notify(<EventData>{ eventName: "livesync", object: app });
8484
const liveSyncCore = global.__onLiveSyncCore;
8585
let reapplyAppStyles = false;
86-
let reapplyLocalStyles = false;
8786

87+
// ModuleContext is available only for Hot Module Replacement
8888
if (context && context.path) {
89-
const extensions = ["css", "scss"];
89+
const styleExtensions = ["css", "scss"];
9090
const appStylesFullFileName = getCssFileName();
9191
const appStylesFileName = appStylesFullFileName.substring(0, appStylesFullFileName.lastIndexOf(".") + 1);
92-
reapplyAppStyles = extensions.some(ext => context.path === appStylesFileName.concat(ext));
93-
if (!reapplyAppStyles) {
94-
reapplyLocalStyles = extensions.some(ext => context.path.endsWith(ext));
95-
}
92+
reapplyAppStyles = styleExtensions.some(ext => context.path === appStylesFileName.concat(ext));
9693
}
9794

95+
// Handle application styles
9896
if (reapplyAppStyles && rootView) {
9997
rootView._onCssStateChange();
10098
} else if (liveSyncCore) {
101-
reapplyLocalStyles ? liveSyncCore(context) : liveSyncCore();
99+
liveSyncCore(context);
102100
}
103101
}
104102

tns-core-modules/application/application.ios.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
notify, launchEvent, resumeEvent, suspendEvent, exitEvent, lowMemoryEvent,
1111
orientationChangedEvent, setApplication, livesync, displayedEvent, getCssFileName
1212
} from "./application-common";
13+
import { ModuleType } from "../ui/core/view/view-common";
1314

1415
// First reexport so that app module is initialized.
1516
export * from "./application-common";
@@ -106,6 +107,7 @@ class IOSApplication implements IOSApplicationDefinition {
106107
get delegate(): typeof UIApplicationDelegate {
107108
return this._delegate;
108109
}
110+
109111
set delegate(value: typeof UIApplicationDelegate) {
110112
if (this._delegate !== value) {
111113
this._delegate = value;
@@ -228,8 +230,16 @@ class IOSApplication implements IOSApplicationDefinition {
228230
}
229231

230232
public _onLivesync(context?: ModuleContext): void {
231-
// If view can't handle livesync set window controller.
232-
if (this._rootView && !this._rootView._onLivesync(context)) {
233+
// Handle application root module
234+
const isAppRootModuleChanged = context && context.path && context.path.includes(getMainEntry().moduleName) && context.type !== ModuleType.style;
235+
236+
// Set window content when:
237+
// + Application root module is changed
238+
// + View did not handle the change
239+
// Note:
240+
// The case when neither app root module is changed, nor livesync is handled on View,
241+
// then changes will not apply until navigate forward to the module.
242+
if (isAppRootModuleChanged || (this._rootView && !this._rootView._onLivesync(context))) {
233243
this.setWindowContent();
234244
}
235245
}
@@ -258,7 +268,6 @@ class IOSApplication implements IOSApplicationDefinition {
258268
this._window.makeKeyAndVisible();
259269
}
260270
}
261-
262271
}
263272

264273
const iosApp = new IOSApplication();

0 commit comments

Comments
 (0)