diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..d1cfc13a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Karma Tests", + "sourceMaps": true, + "webRoot": "${workspaceRoot}", + "urlFilter": "http://localhost:9876/debug.html", + // "runtimeArgs": [ + // "--headless" + // ], + "pathMapping": { + "/": "${workspaceRoot}", + "/base/": "${workspaceRoot}/" + }, + "sourceMapPathOverrides": { + "webpack:///./*": "${webRoot}/*", + "webpack:///src/*": "${webRoot}/*", + "webpack:///*": "*", + "webpack:///./~/*": "${webRoot}/node_modules/*", + "meteor://💻app/*": "${webRoot}/*" + } + } + ] +} diff --git a/karma.conf.js b/karma.conf.js index 1facd78b..6123e4cc 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -71,9 +71,14 @@ module.exports = function(config) { // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Chrome'], - + browsers: ['ChromeDebugging'], + customLaunchers: { + ChromeDebugging: { + base: 'Chrome', + flags: ['--remote-debugging-port=9333'] + } + }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: false diff --git a/src/app-router.js b/src/app-router.js index 35ad0bdd..fc098418 100644 --- a/src/app-router.js +++ b/src/app-router.js @@ -138,12 +138,20 @@ export class AppRouter extends Router { this.isNavigatingForward = true; } else if (this.currentNavigationTracker > navtracker) { this.isNavigatingBack = true; - } if (!navtracker) { + } + if (!navtracker) { navtracker = Date.now(); this.history.setState('NavigationTracker', navtracker); } this.currentNavigationTracker = navtracker; + let historyIndex = this.history.getHistoryIndex(); + this.lastHistoryMovement = historyIndex - this.currentHistoryIndex; + if (isNaN(this.lastHistoryMovement)) { + this.lastHistoryMovement = 0; + } + this.currentHistoryIndex = historyIndex; + instruction.previousInstruction = this.currentInstruction; if (!instructionCount) { @@ -256,7 +264,11 @@ function resolveInstruction(instruction, result, isInnerInstruction, router) { function restorePreviousLocation(router) { let previousLocation = router.history.previousLocation; if (previousLocation) { - router.navigate(router.history.previousLocation, { trigger: false, replace: true }); + Promise.resolve().then(() => { + if (router.lastHistoryMovement && !isNaN(router.lastHistoryMovement)) { + router.history.go(-router.lastHistoryMovement); + } + }); } else if (router.fallbackRoute) { router.navigate(router.fallbackRoute, { trigger: true, replace: true }); } else { diff --git a/src/router.js b/src/router.js index 03844d09..a741f5a3 100644 --- a/src/router.js +++ b/src/router.js @@ -94,6 +94,16 @@ export class Router { */ currentNavigationTracker: number; + /** + * The current index in the browser history. + */ + currentHistoryIndex: number; + + /** + * The amount of index steps in the last history navigation + */ + lastHistoryMovement: number; + /** * The navigation models for routes that specified [[RouteConfig.nav]]. */ diff --git a/test/app-router.spec.js b/test/app-router.spec.js index 1933f8e1..36f798bd 100644 --- a/test/app-router.spec.js +++ b/test/app-router.spec.js @@ -1,24 +1,74 @@ -import {History} from 'aurelia-history'; +import {History,BrowserHistory,LinkHandler} from 'aurelia-history'; import {Container} from 'aurelia-dependency-injection'; import {AppRouter} from '../src/app-router'; import {RouteLoader} from '../src/route-loading'; import {Pipeline} from '../src/pipeline'; +import {PipelineProvider} from '../src/pipeline-provider'; +// import 'aurelia-polyfills'; class MockHistory extends History { + history; activate() {} deactivate() {} - navigate() {} + navigate(location) { + this.history.navigate(location) + } navigateBack() {} - setState(key, value) {} + setState(key, value) { + this.history.setState(key, value); + } getState(key) { - return null; + return this.history.getState(key); + } + getHistoryIndex() { + let historyIndex = this.getState('HistoryIndex'); + if (historyIndex === undefined) { + historyIndex = this.history.length - 1; + this.setState('HistoryIndex', historyIndex); + } + return historyIndex; + }; + go(movement) { + return this.history.go(movement); + } +} + +class MockBrowserHistory { + currentLocationIndex = 0; + states = []; + get length() { + return this.states.length; + } + setState(key, value) { + if (!this.states[this.currentLocationIndex]) { + this.states[this.currentLocationIndex] = {}; + } + this.states[this.currentLocationIndex][key] = value; + } + getState(key) { + if (!this.states[this.currentLocationIndex]) { + this.states[this.currentLocationIndex] = {}; + } + return this.states[this.currentLocationIndex][key]; + } + location(location) { + this.states.splice(this.currentLocationIndex + 1); + this.currentLocationIndex = this.states.length; + this.states.push({ HistoryIndex: this.currentLocationIndex }); + return location; + } + go(movement) { + this.currentLocationIndex += movement; + console.log('GO', this.currentLocationIndex, this.states); } } class MockLoader extends RouteLoader { loadRoute(router, config) { return Promise.resolve({ - viewModel: {} + viewModel: { + canDeactivate: () => { console.log('canDeactivate'); return false; } + } }); } } @@ -42,6 +92,7 @@ describe('app-router', () => { beforeEach(() => { history = new MockHistory(); + history.history = new MockBrowserHistory(); container = new Container(); container.registerSingleton(RouteLoader, MockLoader); ea = { publish() {} }; @@ -208,12 +259,17 @@ describe('app-router', () => { describe('loadUrl', () => { it('restores previous location when route not found', (done) => { spyOn(history, 'navigate'); + spyOn(history, 'go'); - router.history.previousLocation = 'prev'; - router.loadUrl('next') + router.history.previousLocation = router.history.history.location('prev'); + + router.lastHistoryMovement = 1; + router.loadUrl(router.history.history.location('next')) .then(result => { expect(result).toBeFalsy(); - expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true }); + // Navigation is now restored through the browser history + // expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true }); + expect(history.go).toHaveBeenCalledWith(-1); }) .catch(result => expect(true).toBeFalsy('should have succeeded')) .then(done); @@ -235,8 +291,10 @@ describe('app-router', () => { it('restores previous location on error', (done) => { spyOn(history, 'navigate'); + spyOn(history, 'go'); + + router.history.previousLocation = router.history.history.location('prev'); - router.history.previousLocation = 'prev'; router.activate(); router.configure(config => { config.map([ @@ -244,14 +302,57 @@ describe('app-router', () => { ]); }); - router.loadUrl('next') + router.lastHistoryMovement = 1; + router.loadUrl(router.history.history.location('next')) .then(result => { expect(result).toBeFalsy(); - expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true }); + // Navigation is now restored through the browser history + // expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true }); + expect(router.history.go).toHaveBeenCalledWith(-1); }) .catch(result => expect(true).toBeFalsy('should have succeeded')) .then(done); }); + + // NOT WORKING + it('restores previous location after history navigation', (done) => { + spyOn(history, 'navigate'); + spyOn(history, 'go'); + + const provider = new PipelineProvider(container); + const router = new AppRouter(container, history, provider, ea); + + router.activate(); + router.configure(config => { + config.map([ + { name: 'first', route: 'first', moduleId: './first' }, + { name: 'second', route: 'second', moduleId: './second' }, + { name: 'third', route: 'third', moduleId: './third' }, + ]); + }).then(() => { + router.loadUrl(router.history.history.location('first')) + .then(() => router.loadUrl(router.history.history.location('second'))) + .then(() => router.loadUrl(router.history.history.location('third'))) + .then(result => { + expect(result).toBeTruthy(); + }) + .catch(result => expect(true).toBeFalsy('should have succeeded')) + .then(done); + }); + + + // router.lastHistoryMovement = 1; + // router.loadUrl(router.history.history.location('next')) + // .then(result => { + // expect(result).toBeFalsy(); + // // Navigation is now restored through the browser history + // // expect(history.navigate).toHaveBeenCalledWith('#/prev', { trigger: false, replace: true }); + // expect(router.history.go).toHaveBeenCalledWith(-1); + // }) + // .catch(result => expect(true).toBeFalsy('should have succeeded')) + // .then(done); + }); + }); describe('instruction completes as navigation command', () => { it('should complete instructions in order before terminating', done => {