diff --git a/e2e/modal-navigation-ng/app/app.css b/e2e/modal-navigation-ng/app/app.css index d23504c4e..5e41a4552 100644 --- a/e2e/modal-navigation-ng/app/app.css +++ b/e2e/modal-navigation-ng/app/app.css @@ -10,3 +10,6 @@ of writing your own CSS rules. For a full list of class names in the theme refer to http://docs.nativescript.org/ui/theme. */ @import '~nativescript-theme-core/css/core.light.css'; +Button { + font-size: 10px; +} diff --git a/e2e/modal-navigation-ng/app/app.module.ts b/e2e/modal-navigation-ng/app/app.module.ts index 2afa7f59a..9251b6661 100644 --- a/e2e/modal-navigation-ng/app/app.module.ts +++ b/e2e/modal-navigation-ng/app/app.module.ts @@ -2,6 +2,7 @@ import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core"; import { NativeScriptModule } from "nativescript-angular/nativescript.module"; import { AppRoutingModule } from "./app.routing"; import { AppComponent } from "./app.component"; +import { NamedRouterComponent } from "./named-router.component"; import { TabComponent } from "./tab.component"; import { LayoutComponent } from "./layout.component"; @@ -29,16 +30,18 @@ traceEnable(); AppRoutingModule ], entryComponents: [ - AppComponent, - TabComponent, - LayoutComponent, - ModalRouterComponent, - NestedModalComponent, - ModalComponent, + AppComponent, + NamedRouterComponent, + TabComponent, + LayoutComponent, + ModalRouterComponent, + NestedModalComponent, + ModalComponent, ModalViewComponent ], declarations: [ AppComponent, + NamedRouterComponent, TabComponent, LayoutComponent, HomeComponent, @@ -75,8 +78,9 @@ export class AppModule { static bootstrapRootComponent() { const options = { 'page-router': AppComponent, + 'named-page-router': NamedRouterComponent, 'tab': TabComponent, - 'layout': LayoutComponent + 'layout': LayoutComponent }; const component = options[AppModule.root]; diff --git a/e2e/modal-navigation-ng/app/app.routing.ts b/e2e/modal-navigation-ng/app/app.routing.ts index f4b156db3..0a49e8251 100644 --- a/e2e/modal-navigation-ng/app/app.routing.ts +++ b/e2e/modal-navigation-ng/app/app.routing.ts @@ -40,6 +40,34 @@ const routes: Routes = [ } ]; +const namedOutletRoutes: Routes = [ + { path: "", redirectTo: "/(namedRouter:home)", pathMatch: "full" }, + { + path: "home", component: HomeComponent, outlet: "namedRouter", children: [ + { + path: "modal", component: ModalComponent, children: [ + { path: "nested-frame-modal", component: NestedModalComponent }] + }, + { path: "modal-second", component: ModalSecondComponent } + ] + }, + { + path: "second", outlet: "namedRouter", component: SecondComponent, children: [ + { + path: "modal", component: ModalComponent, children: [ + { path: "nested-frame-modal", component: NestedModalComponent }] + }, + { path: "modal-second", component: ModalSecondComponent } + ] + }, + { + path: "modal-shared", component: ModalViewContentComponent, outlet: "modalOutlet" + }, + { + path: "modal-shared-second-host", outlet: "namedRouter", component: ModalSharedSecondComponent + } +]; + const routesTab: Routes = [ { path: "", redirectTo: "/home(secondOutlet:second)", pathMatch: "full" }, { @@ -96,6 +124,8 @@ export class AppRoutingModule { this.router.resetConfig(routes); } else if (AppModule.root === "layout") { this.router.resetConfig(routesLayout); + } else if (AppModule.root === "named-page-router") { + this.router.resetConfig(namedOutletRoutes); } else { this.router.resetConfig(routesTab); } diff --git a/e2e/modal-navigation-ng/app/home/home.component.html b/e2e/modal-navigation-ng/app/home/home.component.html index 3f1172827..9601e29ce 100644 --- a/e2e/modal-navigation-ng/app/home/home.component.html +++ b/e2e/modal-navigation-ng/app/home/home.component.html @@ -1,11 +1,12 @@ - + - + + @@ -13,5 +14,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/e2e/modal-navigation-ng/app/home/home.component.ts b/e2e/modal-navigation-ng/app/home/home.component.ts index df5a57139..8fc08fb7c 100644 --- a/e2e/modal-navigation-ng/app/home/home.component.ts +++ b/e2e/modal-navigation-ng/app/home/home.component.ts @@ -61,6 +61,11 @@ export class HomeComponent { AppModule.platformRef._livesync(); } + onNamedFrameRootViewReset() { + AppModule.root = "named-page-router"; + AppModule.platformRef._livesync(); + } + onTabRootViewReset() { AppModule.root = "tab"; AppModule.platformRef._livesync(); diff --git a/e2e/modal-navigation-ng/app/layout.component.html b/e2e/modal-navigation-ng/app/layout.component.html index 96f4323fc..3d50caa77 100644 --- a/e2e/modal-navigation-ng/app/layout.component.html +++ b/e2e/modal-navigation-ng/app/layout.component.html @@ -1,9 +1,10 @@ - - + + + - \ No newline at end of file + \ No newline at end of file diff --git a/e2e/modal-navigation-ng/app/layout.component.ts b/e2e/modal-navigation-ng/app/layout.component.ts index 93407ca2f..de4c8eb45 100644 --- a/e2e/modal-navigation-ng/app/layout.component.ts +++ b/e2e/modal-navigation-ng/app/layout.component.ts @@ -67,6 +67,11 @@ export class LayoutComponent { AppModule.platformRef._livesync(); } + onNamedFrameRootViewReset() { + AppModule.root = "named-page-router"; + AppModule.platformRef._livesync(); + } + onTabRootViewReset() { AppModule.root = "tab"; AppModule.platformRef._livesync(); diff --git a/e2e/modal-navigation-ng/app/named-router.component.html b/e2e/modal-navigation-ng/app/named-router.component.html new file mode 100644 index 000000000..83ba115c4 --- /dev/null +++ b/e2e/modal-navigation-ng/app/named-router.component.html @@ -0,0 +1 @@ + diff --git a/e2e/modal-navigation-ng/app/named-router.component.ts b/e2e/modal-navigation-ng/app/named-router.component.ts new file mode 100644 index 000000000..e00c929a3 --- /dev/null +++ b/e2e/modal-navigation-ng/app/named-router.component.ts @@ -0,0 +1,27 @@ +import { Component, ViewContainerRef } from "@angular/core"; +import { Router, NavigationEnd } from "@angular/router"; +import { NSLocationStrategy } from "nativescript-angular/router/ns-location-strategy"; + +import { ViewContainerRefService } from "./shared/ViewContainerRefService"; + +@Component({ + selector: "named-router", + templateUrl: "named-router.component.html", +}) + +export class NamedRouterComponent { + constructor( + router: Router, + location: NSLocationStrategy, + private _vcRef: ViewContainerRef, + private _viewContainerRefService: ViewContainerRefService) { + router.events.subscribe(e => { + if (e instanceof NavigationEnd) { + console.log("[ROUTER]: " + e.toString()); + console.log(location.toString()); + } + }); + + this._viewContainerRefService.root = this._vcRef; + } +} diff --git a/e2e/modal-navigation-ng/e2e/modal-frame.e2e-spec.ts b/e2e/modal-navigation-ng/e2e/modal-frame.e2e-spec.ts new file mode 100644 index 000000000..2c9b95206 --- /dev/null +++ b/e2e/modal-navigation-ng/e2e/modal-frame.e2e-spec.ts @@ -0,0 +1,77 @@ +import { AppiumDriver, createDriver } from "nativescript-dev-appium"; +import { Screen } from "./screen" +import { + roots, + modalFrameBackground, + testSecondPageBackground, + testSecondPageClose, + testNestedModalFrameBackground, + testNestedModalPageBackground, + testDialogBackground +} from "./shared.e2e-spec" + +describe("modal-frame:", () => { + + let driver: AppiumDriver; + let screen: Screen; + + before(async () => { + driver = await createDriver(); + screen = new Screen(driver); + }); + + roots.forEach(root => { + describe(`${root} modal frame background scenarios:`, () => { + + before(async () => { + await screen[root](); + }); + + beforeEach(async function () { + await screen.loadModalFrame(); + }); + + afterEach(async function () { + if (this.currentTest.state === "failed") { + await driver.logPageSource(this.currentTest.title); + await driver.logScreenshot(this.currentTest.title); + await driver.resetApp(); + await screen[root](); + } + }); + + after(async () => { + await screen.closeModal(); + await screen.loadedHome(); + }); + + it("should show dialog confirm, run in background", async () => { + await testDialogBackground(driver, screen); + }); + + it("should run modal page with frame in background", async () => { + await modalFrameBackground(driver, screen); + }); + + it("should navigate to second page, run in background, go back", async () => { + await testSecondPageBackground(driver, screen); + }); + + it("should show nested modal page with frame, run in background, close", async () => { + await testNestedModalFrameBackground(driver, screen); + }); + + it("should show nested modal page, run in background, close", async () => { + await testNestedModalPageBackground(driver, screen); + }); + + it("should navigate to second page, close", async () => { + await testSecondPageClose(driver, screen); + }); + + it("should navigate to second page, run in background, go back", async () => { + await testSecondPageBackground(driver, screen); + }); + }); + }); +}); diff --git a/e2e/modal-navigation-ng/e2e/modal-layout.e2e-spec.ts b/e2e/modal-navigation-ng/e2e/modal-layout.e2e-spec.ts new file mode 100644 index 000000000..9cd95bea6 --- /dev/null +++ b/e2e/modal-navigation-ng/e2e/modal-layout.e2e-spec.ts @@ -0,0 +1,53 @@ +import { AppiumDriver, createDriver } from "nativescript-dev-appium"; +import { Screen } from "./screen" +import { + roots, + modalFrameBackground, + testSecondPageBackground, + testSecondPageClose, + testNestedModalFrameBackground, + testNestedModalPageBackground, + testDialogBackground, +} from "./shared.e2e-spec" + +describe("modal-layout:", () => { + + let driver: AppiumDriver; + let screen: Screen; + + before(async () => { + driver = await createDriver(); + screen = new Screen(driver); + }); + + roots.forEach(root => { + describe(`${root} modal no frame background scenarios:`, () => { + + before(async () => { + await screen[root](); + }); + + beforeEach(async function () { + await screen.loadModalNoFrame(); + }); + + afterEach(async function () { + if (this.currentTest.state === "failed") { + await driver.logPageSource(this.currentTest.title); + await driver.logScreenshot(this.currentTest.title); + await driver.resetApp(); + await screen[root](); + } + }); + + after(async () => { + await screen.closeModal(); + await screen.loadedHome(); + }); + + it("should show dialog confirm inside modal view with no frame, run in background", async () => { + await testDialogBackground(driver, screen, false); + }); + }); + }); +}); diff --git a/e2e/modal-navigation-ng/e2e/modal.shared.e2e-spec.ts b/e2e/modal-navigation-ng/e2e/modal.shared.e2e-spec.ts index ecdc240b4..75f4867ad 100644 --- a/e2e/modal-navigation-ng/e2e/modal.shared.e2e-spec.ts +++ b/e2e/modal-navigation-ng/e2e/modal.shared.e2e-spec.ts @@ -1,6 +1,8 @@ import { AppiumDriver, createDriver, SearchOptions } from "nativescript-dev-appium"; import { assert } from "chai"; +const homeComponent = "Home Component"; + describe("Shared modal from home and back", () => { let driver: AppiumDriver; @@ -22,7 +24,7 @@ describe("Shared modal from home and back", () => { }); it ("should find home component", async () => { - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); it("should open/close shared modal from home component", async () => { @@ -31,7 +33,7 @@ describe("Shared modal from home and back", () => { }); it ("should find home component again", async () => { - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); }); @@ -56,7 +58,7 @@ describe("Shared modal from second and back", () => { }); it ("should find home component", async () => { - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); it ("should navigate to second component", async() => { @@ -98,7 +100,7 @@ describe("Shared modal from different components", () => { }); it ("should find home component", async () => { - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); it("should open/close shared modal from home component", async () => { @@ -107,7 +109,7 @@ describe("Shared modal from different components", () => { }); it ("should find home component again", async () => { - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); it ("should navigate to second component", async() => { @@ -129,7 +131,7 @@ describe("Shared modal from different components", () => { it ("should navigate back to home component", async () => { await goBack(driver); - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); it("should open/close shared modal from home component after manipulations with second", async () => { @@ -138,7 +140,7 @@ describe("Shared modal from different components", () => { }); it ("should find home component again", async () => { - await assertComponent(driver, "home component"); + await assertComponent(driver, homeComponent); }); }); diff --git a/e2e/modal-navigation-ng/e2e/screen.ts b/e2e/modal-navigation-ng/e2e/screen.ts new file mode 100644 index 000000000..9033d90e1 --- /dev/null +++ b/e2e/modal-navigation-ng/e2e/screen.ts @@ -0,0 +1,259 @@ +import { AppiumDriver } from "nativescript-dev-appium"; +import { assert } from "chai"; + +const home = "Home Component" +const first = "First" +const modal = "Modal"; +const modalFirst = "Modal First"; +const dialogConfirm = "Dialog"; +const modalSecond = "Modal Second"; +const modalNested = "Modal Nested"; + +const modalFrame = "Show Modal Page With Frame"; +const modalNoFrame = "Show Modal Without Frame"; +const modalLayout = "Show Modal Layout"; +const modalTabView = "Show Modal TabView"; +const navToSecondPage = "Navigate To Second Page"; +const showDialog = "Show Dialog"; +const resetFrameRootView = "Reset Frame Root View"; +const resetNamedFrameRootView = "Reset Named Frame Root View"; +const resetTabRootView = "Reset Tab Root View"; +const resetLayoutRootView = "Reset Layout Root View"; + +const showNestedModalFrame = "Show Nested Modal Page With Frame"; +const showNestedModalPage = "Show Nested Modal Page"; + +const confirmDialog = "Yes"; +const confirmDialogMessage = "Message"; +const closeModalNested = "Close Modal Nested"; +const closeModal = "Close Modal"; +const goBack = "Go Back"; + +export class Screen { + + private _driver: AppiumDriver + + constructor(driver: AppiumDriver) { + this._driver = driver; + } + + loadedHome = async () => { + const lblHome = await this._driver.findElementByText(home); + assert.isTrue(await lblHome.isDisplayed()); + console.log(home + " loaded!"); + } + + resetFrameRootView = async () => { + console.log("Setting frame root ..."); + const btnResetFrameRootView = await this._driver.findElementByText(resetFrameRootView); + await btnResetFrameRootView.tap(); + } + + resetNamedFrameRootView = async () => { + console.log("Setting named frame root ..."); + const btnResetFrameRootView = await this._driver.findElementByText(resetNamedFrameRootView); + await btnResetFrameRootView.tap(); + } + + resetLayoutRootView = async () => { + console.log("Setting layout root ..."); + const btnResetLayoutRootView = await this._driver.findElementByText(resetLayoutRootView); + await btnResetLayoutRootView.tap(); + } + + resetTabRootView = async () => { + const btnResetTabRootView = await this._driver.findElementByText(resetTabRootView); + await btnResetTabRootView.tap(); + } + + loadedTabRootView = async () => { + const tabFirst = await this._driver.findElementByText(first); + assert.isTrue(await tabFirst.isDisplayed()); + console.log("Tab root view loaded!"); + } + + setFrameRootView = async () => { + // should load frame root, no need to verify it is loaded + await this.loadedHome(); + await this.resetFrameRootView(); + } + + setNamedFrameRootView = async () => { + // should load named frame root, no need to verify it is loaded + await this.loadedHome(); + await this.resetNamedFrameRootView(); + } + + setTabRootView = async () => { + // should load tab root + await this.loadedHome(); + try { + await this.loadedTabRootView(); + } catch (err) { + await this.resetTabRootView(); + await this.loadedTabRootView(); + } + } + + setLayoutRootView = async () => { + // should load layout root, no need to verify it is loaded + await this.loadedHome(); + await this.resetLayoutRootView(); + } + + showModalFrame = async () => { + const btnModalFrame = await this._driver.findElementByText(modalFrame); + await btnModalFrame.tap(); + } + + loadedModalFrame = async () => { + const lblModal = await this._driver.findElementByText(modal); + assert.isTrue(await lblModal.isDisplayed()); + console.log(modal + " loaded!"); + } + + showModalNoFrame = async () => { + const btnModalPage = await this._driver.findElementByText(modalNoFrame); + await btnModalPage.tap(); + } + + loadedModalPage = async () => { + const btnShowNestedModalPage = await this._driver.findElementByText(showNestedModalPage); + assert.isTrue(await btnShowNestedModalPage.isDisplayed()); + console.log("Modal Page loaded!"); + } + + showModalLayout = async () => { + const btnModalLayout = await this._driver.findElementByText(modalLayout); + await btnModalLayout.tap(); + } + + loadedModalLayout = async () => { + await this.loadedModalFrame(); + } + + showModalTabView = async () => { + const btnModalTabView = await this._driver.findElementByText(modalTabView); + await btnModalTabView.tap(); + } + + loadedModalTabView = async () => { + const itemModalFirst = await this._driver.findElementByText(modalFirst); + assert.isTrue(await itemModalFirst.isDisplayed()); + console.log("Modal TabView loaded!"); + } + + navigateToSecondPage = async () => { + const btnNavToSecondPage = await this._driver.findElementByText(navToSecondPage); + await btnNavToSecondPage.tap(); + } + + showDialogConfirm = async () => { + const btnShowDialogConfirm = await this._driver.findElementByText(showDialog); + await btnShowDialogConfirm.tap(); + } + + navigateToFirstItem = async () => { + const itemModalFirst = await this._driver.findElementByText(modalFirst); + await itemModalFirst.tap(); + } + + navigateToSecondItem = async () => { + const itemModalSecond = await this._driver.findElementByText(modalSecond); + await itemModalSecond.tap(); + } + + loadedModalNoFrame = async () => { + const btnShowDialogConfirm = await this._driver.findElementByText(showDialog); + const btnCloseModal = await this._driver.findElementByText(closeModal); + assert.isTrue(await btnShowDialogConfirm.isDisplayed()); + assert.isTrue(await btnCloseModal.isDisplayed()); + console.log("Modal Without Frame shown!"); + } + + loadedConfirmDialog = async () => { + const lblDialogMessage = await this._driver.findElementByText(confirmDialogMessage); + assert.isTrue(await lblDialogMessage.isDisplayed()); + console.log(dialogConfirm + " shown!"); + } + + loadedSecondPage = async () => { + const lblModalSecond = await this._driver.findElementByText(modalSecond); + assert.isTrue(await lblModalSecond.isDisplayed()); + console.log(modalSecond + " loaded!"); + } + + loadedFirstItem = async () => { + const lblModal = await this._driver.findElementByText(modal); + assert.isTrue(await lblModal.isDisplayed()); + console.log("First Item loaded!"); + } + + loadedSecondItem = async () => { + const btnGoBack = await this._driver.findElementByText(goBack); + assert.isTrue(await btnGoBack.isDisplayed()); + console.log("Second Item loaded!"); + } + + closeDialog = async () => { + const btnYesDialog = await this._driver.findElementByText(confirmDialog); + await btnYesDialog.tap(); + } + + goBackFromSecondPage = async () => { + const btnGoBackFromSecondPage = await this._driver.findElementByText(goBack); + await btnGoBackFromSecondPage.tap(); + } + + showNestedModalFrame = async () => { + const btnShowNestedModalFrame = await this._driver.findElementByText(showNestedModalFrame); + await btnShowNestedModalFrame.tap(); + } + + loadedNestedModalFrame = async () => { + const lblModalNested = await this._driver.findElementByText(modalNested); + assert.isTrue(await lblModalNested.isDisplayed()); + console.log(modalNested + " loaded!"); + } + + closeModalNested = async () => { + const btnCloseNestedModal = await this._driver.findElementByText(closeModalNested); + await btnCloseNestedModal.tap(); + } + + showNestedModalPage = async () => { + const btnShowNestedModalPage = await this._driver.findElementByText(showNestedModalPage); + await btnShowNestedModalPage.tap(); + } + + loadedNestedModalPage = async () => { + const btnCloseModalNested = await this._driver.findElementByText(closeModalNested); + assert.isTrue(await btnCloseModalNested.isDisplayed()); + console.log(closeModalNested + " loaded!"); + } + + closeModal = async () => { + const btnCloseModal = await this._driver.findElementByText(closeModal); + await btnCloseModal.tap(); + } + + loadModalNoFrame = async () => { + try { + await this.loadedModalNoFrame(); + } catch (err) { + // should show modal with no frame + await this.showModalNoFrame(); + await this.loadedModalNoFrame(); + } + } + + loadModalFrame = async () => { + try { + await this.loadedModalFrame(); + } catch (err) { + // should show modal page with frame + await this.showModalFrame(); + await this.loadedModalFrame(); + } + } +} \ No newline at end of file diff --git a/e2e/modal-navigation-ng/e2e/shared.e2e-spec.ts b/e2e/modal-navigation-ng/e2e/shared.e2e-spec.ts new file mode 100644 index 000000000..f0c6e3192 --- /dev/null +++ b/e2e/modal-navigation-ng/e2e/shared.e2e-spec.ts @@ -0,0 +1,86 @@ +import { AppiumDriver, createDriver } from "nativescript-dev-appium"; +import { Screen } from "./screen" + +const time = 1; + +export const roots = ["setFrameRootView", "setLayoutRootView", "setTabRootView", "setNamedFrameRootView"]; + +export async function modalFrameBackground(driver: AppiumDriver, screen: Screen) { + await driver.backgroundApp(time); + await screen.loadedModalFrame(); +} + +export async function testDialogBackground(driver: AppiumDriver, screen: Screen, isInFrame: boolean = true) { + await screen.showDialogConfirm(); + await screen.loadedConfirmDialog(); + + await driver.backgroundApp(time); + await screen.loadedConfirmDialog(); + + await screen.closeDialog(); + if (isInFrame) { + await screen.loadedModalFrame(); + } +} + +export async function testSecondPageBackground(driver: AppiumDriver, screen: Screen) { + await screen.navigateToSecondPage(); + await screen.loadedSecondPage(); + + await driver.backgroundApp(time); + await screen.loadedSecondPage(); + + await screen.goBackFromSecondPage(); + await screen.loadedModalFrame(); +} + +export async function testSecondPageClose(driver: AppiumDriver, screen: Screen) { + await screen.navigateToSecondPage(); + await screen.loadedSecondPage(); + + await screen.closeModal(); + await screen.loadedHome(); +} + +export async function testNestedModalFrameBackground(driver: AppiumDriver, screen: Screen, isInFrame: boolean = true) { + await screen.showNestedModalFrame(); + await screen.loadedNestedModalFrame(); + + await driver.backgroundApp(time); + await screen.loadedNestedModalFrame(); + + await screen.closeModalNested(); + isInFrame ? await screen.loadedModalFrame() : await screen.loadedModalPage(); +} + +export async function testNestedModalPageBackground(driver: AppiumDriver, screen: Screen, isInFrame: boolean = true) { + await screen.showNestedModalPage(); + await screen.loadedNestedModalPage(); + + await driver.backgroundApp(time); + await screen.loadedNestedModalPage(); + + await screen.closeModalNested(); + isInFrame ? await screen.loadedModalFrame() : await screen.loadedModalPage(); +} + +export async function modalPageBackground(driver: AppiumDriver, screen: Screen, isInFrame: boolean = true) { + await driver.backgroundApp(time); + isInFrame ? await screen.loadedModalFrame() : await screen.loadedModalPage(); +} + +export async function modalTabViewBackground(driver: AppiumDriver, screen: Screen) { + await driver.backgroundApp(time); + await screen.loadedModalTabView(); +} + +export async function testSecondItemBackground(driver: AppiumDriver, screen: Screen) { + await screen.navigateToSecondItem(); + await screen.loadedSecondItem(); + + await driver.backgroundApp(time); + await screen.loadedSecondItem(); + + await screen.navigateToFirstItem(); + await screen.loadedFirstItem(); +} diff --git a/e2e/router-tab-view/package.json b/e2e/router-tab-view/package.json index 28e98d89c..451dcfb34 100644 --- a/e2e/router-tab-view/package.json +++ b/e2e/router-tab-view/package.json @@ -36,10 +36,13 @@ "lazy": "1.0.11", "nativescript-dev-appium": "^3.1.0", "nativescript-dev-typescript": "~0.6.0", - "typescript": "~2.7.2" + "typescript": "~2.7.2", + "@types/chai": "^4.0.2", + "@types/mocha": "^2.2.41", + "@types/node": "^7.0.5" }, "scripts": { - "e2e": "tsc -p e2e && mocha --opts ./e2e/config/mocha.opts", + "e2e": "tsc -p e2e && mocha --opts ../config/mocha.opts --recursive e2e --appiumCapsLocation ../config/appium.capabilities.json", "compile-tests": "tsc -p e2e --watch" } } diff --git a/nativescript-angular/directives/dialogs.ts b/nativescript-angular/directives/dialogs.ts index 612b2788b..961ca362b 100644 --- a/nativescript-angular/directives/dialogs.ts +++ b/nativescript-angular/directives/dialogs.ts @@ -74,7 +74,9 @@ export class ModalDialogService { const componentContainer = moduleRef || viewContainerRef; const resolver = componentContainer.injector.get(ComponentFactoryResolver); - this.location._beginModalNavigation(); + const frame = parentView.page && parentView.page.frame; + + this.location._beginModalNavigation(frame); return new Promise((resolve, reject) => { setTimeout(() => { diff --git a/nativescript-angular/platform-providers.ts b/nativescript-angular/platform-providers.ts index 9c1cef6f6..e6d2ff380 100644 --- a/nativescript-angular/platform-providers.ts +++ b/nativescript-angular/platform-providers.ts @@ -66,7 +66,80 @@ export const defaultPageFactoryProvider = { provide: PAGE_FACTORY, useValue: def @Injectable() export class FrameService { // TODO: Add any methods that are needed to handle frame/page navigation + private frames: { frame: Frame, name: string, rootOutlet: string }[] = []; + + // Return the topmost frame. + // TabView with frames scenario: topmost() will return the root TabViewItem frame, + // which could be the wrong topmost frame (modal with nested frame e.g.): + // TabViewItem -> Frame -> Modal -> Frame2 -> Frame2-Navigation getFrame(): Frame { - return topmost(); + let topmostFrame = topmost(); + const { cachedFrame, cachedFrameRootOutlet } = this.findFrame(topmostFrame); + + if (cachedFrame && cachedFrameRootOutlet) { + const topmostFrameByOutlet = this.getTopmostFrameByOutlet(cachedFrameRootOutlet); + + if (topmostFrameByOutlet && topmostFrameByOutlet !== cachedFrame) { + topmostFrame = topmostFrameByOutlet; + } + } + + return topmostFrame; + } + + addFrame(frame: Frame, name: string, rootOutlet: string) { + this.frames.push({ frame: frame, name: name, rootOutlet: rootOutlet }); + } + + removeFrame(frame: Frame) { + this.frames = this.frames.filter(currentFrame => currentFrame.frame !== frame); + } + + containsOutlet(name: string) { + let nameFound = false; + + for (let i = 0; i < this.frames.length; i++) { + const currentFrame = this.frames[i]; + + if (name && currentFrame.name === name) { + nameFound = true; + break; + } + } + + return nameFound; + } + + findFrame(frame: Frame) { + let cachedFrame; + let cachedFrameRootOutlet; + + for (let i = 0; i < this.frames.length; i++) { + const currentFrame = this.frames[i]; + + if (currentFrame.frame === frame) { + cachedFrame = currentFrame; + cachedFrameRootOutlet = currentFrame.rootOutlet; + break; + } + } + + return { cachedFrame, cachedFrameRootOutlet }; + } + + // Return the latest navigated frame from the given outlet branch. + private getTopmostFrameByOutlet(rootOutlet: string): Frame { + let frame: Frame; + + for (let i = this.frames.length - 1; i > 0; i--) { + const currentFrame = this.frames[i]; + + if (currentFrame.rootOutlet === rootOutlet) { + frame = currentFrame.frame; + break; + } + } + + return frame; } } diff --git a/nativescript-angular/router/ns-location-strategy.ts b/nativescript-angular/router/ns-location-strategy.ts index 9ab581cdd..7cf31e270 100644 --- a/nativescript-angular/router/ns-location-strategy.ts +++ b/nativescript-angular/router/ns-location-strategy.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { LocationStrategy } from "@angular/common"; import { DefaultUrlSerializer, UrlSegmentGroup, UrlTree } from "@angular/router"; import { routerLog } from "../trace"; -import { NavigationTransition } from "tns-core-modules/ui/frame"; +import { NavigationTransition, Frame } from "tns-core-modules/ui/frame"; import { isPresent } from "../lang-facade"; import { FrameService } from "../platform-providers"; @@ -77,7 +77,7 @@ export class NSLocationStrategy extends LocationStrategy { } pushState(state: any, title: string, url: string, queryParams: string): void { - routerLog("NSLocationStrategy.pushState state: " + + routerLog("NSLocationStrategy.pushState state: " + `${state}, title: ${title}, url: ${url}, queryParams: ${queryParams}`); this.pushStateInternal(state, title, url, queryParams); } @@ -240,7 +240,9 @@ export class NSLocationStrategy extends LocationStrategy { private callPopState(state: LocationState, pop: boolean = true) { const urlSerializer = new DefaultUrlSerializer(); - if (state) { + const rootOutlet = this.currentUrlTree.root.children[this.currentOutlet]; + + if (state && rootOutlet) { this.currentUrlTree.root.children[this.currentOutlet] = state.segmentGroup; } else { // when closing modal view there are scenarios (e.g. root viewContainerRef) when we need @@ -283,13 +285,20 @@ export class NSLocationStrategy extends LocationStrategy { } // Methods for syncing with page navigation in PageRouterOutlet - public _beginBackPageNavigation(name: string) { + public _beginBackPageNavigation(name: string, frame: Frame) { routerLog("NSLocationStrategy.startGoBack()"); if (this._isPageNavigationBack) { throw new Error("Calling startGoBack while going back."); } this._isPageNavigationBack = true; - this.currentOutlet = name; + + let { cachedFrame } = this.frameService.findFrame(frame); + + if (cachedFrame) { + this.currentOutlet = cachedFrame.rootOutlet; + } else if (!this.frameService.containsOutlet(name)) { + this.currentOutlet = name; + } } public _finishBackPageNavigation() { @@ -304,9 +313,12 @@ export class NSLocationStrategy extends LocationStrategy { return this._isPageNavigationBack; } - public _beginModalNavigation(): void { + public _beginModalNavigation(frame: Frame): void { routerLog("NSLocationStrategy._beginModalNavigation()"); - const lastState = this.peekState(this.currentOutlet); + + let { cachedFrameRootOutlet } = this.frameService.findFrame(frame); + + const lastState = this.peekState(cachedFrameRootOutlet || this.currentOutlet); if (lastState) { lastState.isModalNavigation = true; @@ -333,19 +345,31 @@ export class NSLocationStrategy extends LocationStrategy { this._isModalClosing = false; } - public _beginPageNavigation(name: string): NavigationOptions { + public _beginPageNavigation(name: string, frame: Frame): NavigationOptions { routerLog("NSLocationStrategy._beginPageNavigation()"); - const lastState = this.peekState(name); + + let { cachedFrame } = this.frameService.findFrame(frame); + + if (cachedFrame) { + this.currentOutlet = cachedFrame.rootOutlet; + } else { + // Changing the current outlet only if navigating in non-cached root outlet. + if (!this.frameService.containsOutlet(name) && this.statesByOutlet[name] /* ensure root outlet exists */) { + this.currentOutlet = name; + } + + this.frameService.addFrame(frame, name, this.currentOutlet); + } + + const lastState = this.peekState(this.currentOutlet); if (lastState) { lastState.isPageNavigation = true; } - this.currentOutlet = name; - const navOptions = this._currentNavigationOptions || defaultNavOptions; if (navOptions.clearHistory) { routerLog("NSLocationStrategy._beginPageNavigation clearing states history"); - this.statesByOutlet[name] = [lastState]; + this.statesByOutlet[this.currentOutlet] = [lastState]; } this._currentNavigationOptions = undefined; diff --git a/nativescript-angular/router/page-router-outlet.ts b/nativescript-angular/router/page-router-outlet.ts index eb5950558..565414386 100644 --- a/nativescript-angular/router/page-router-outlet.ts +++ b/nativescript-angular/router/page-router-outlet.ts @@ -25,6 +25,7 @@ import { DetachedLoader } from "../common/detached-loader"; import { ViewUtil } from "../view-util"; import { NSLocationStrategy } from "./ns-location-strategy"; import { NSRouteReuseStrategy } from "./ns-route-reuse-strategy"; +import { FrameService } from "../platform-providers"; export class PageRoute { activatedRoute: BehaviorSubject; @@ -149,7 +150,8 @@ export class PageRouterOutlet implements OnDestroy { // tslint:disable-line:dire @Inject(DEVICE) device: Device, @Inject(PAGE_FACTORY) private pageFactory: PageFactory, private routeReuseStrategy: NSRouteReuseStrategy, - elRef: ElementRef + elRef: ElementRef, + private frameService: FrameService ) { this.frame = elRef.nativeElement; log("PageRouterOutlet.constructor frame:" + this.frame); @@ -166,6 +168,7 @@ export class PageRouterOutlet implements OnDestroy { // tslint:disable-line:dire // destroyed on modal view closing this.routeReuseStrategy.clearModalCache(this.name); this.parentContexts.onChildOutletDestroyed(this.name); + this.frameService.removeFrame(this.frame); } deactivate(): void { @@ -286,12 +289,12 @@ export class PageRouterOutlet implements OnDestroy { // tslint:disable-line:dire page.on(Page.navigatedFromEvent, (global).Zone.current.wrap((args: NavigatedData) => { if (args.isBackNavigation) { - this.locationStrategy._beginBackPageNavigation(this.name); + this.locationStrategy._beginBackPageNavigation(this.name, this.frame); this.locationStrategy.back(); } })); - const navOptions = this.locationStrategy._beginPageNavigation(this.name); + const navOptions = this.locationStrategy._beginPageNavigation(this.name, this.frame); // Clear refCache if navigation with clearHistory if (navOptions.clearHistory) { diff --git a/tests/app/tests/ns-location-strategy.ts b/tests/app/tests/ns-location-strategy.ts index 5519075d1..01824f0ec 100644 --- a/tests/app/tests/ns-location-strategy.ts +++ b/tests/app/tests/ns-location-strategy.ts @@ -134,11 +134,11 @@ function createState(url: string, function simulatePageNavigation(strategy: NSLocationStrategy, url: string, outletName: string) { strategy.pushState(null, null, url, null); - strategy._beginPageNavigation(outletName); + strategy._beginPageNavigation(outletName, null); } function simulatePageBack(strategy: NSLocationStrategy, outletName: string) { - strategy._beginBackPageNavigation(outletName); + strategy._beginBackPageNavigation(outletName, null); strategy.back(); strategy._finishBackPageNavigation(); }