diff --git a/lib/router/route-info.ts b/lib/router/route-info.ts index e65f71e..15e212a 100644 --- a/lib/router/route-info.ts +++ b/lib/router/route-info.ts @@ -250,6 +250,7 @@ export default class InternalRouteInfo { throwIfAborted(transition); return route; }) + .then(() => transition._pausingPromise || Promise.resolve(null)) .then(() => this.runBeforeModelHook(transition)) .then(() => throwIfAborted(transition)) .then(() => this.getModel(transition)) diff --git a/lib/router/router.ts b/lib/router/router.ts index cd73f4c..cf5ee46 100644 --- a/lib/router/router.ts +++ b/lib/router/router.ts @@ -137,8 +137,10 @@ export default abstract class Router { this.routeWillChange(newTransition); - newTransition.promise = newTransition.promise!.then( - (result: TransitionState | Route | Error | undefined) => { + let transitionPromise = newTransition.promise!; + newTransition.promise = (newTransition._pausingPromise || Promise.resolve(null)) + .then(() => transitionPromise) + .then((result: TransitionState | Route | Error | undefined) => { if (!newTransition.isAborted) { this._updateURL(newTransition, oldState); this.didTransition(this.currentRouteInfos!); diff --git a/lib/router/transition.ts b/lib/router/transition.ts index 88be275..1316eeb 100644 --- a/lib/router/transition.ts +++ b/lib/router/transition.ts @@ -71,6 +71,7 @@ export default class Transition implements Partial> isCausedByInitialTransition = false; isCausedByAbortingReplaceTransition = false; _visibleQueryParams: Dict = {}; + _pausingPromise?: Promise; isIntermediate = false; [REDIRECT_DESTINATION_SYMBOL]?: Transition; @@ -290,6 +291,10 @@ export default class Transition implements Partial> return this; } + waitFor(promise: Promise) { + this._pausingPromise = promise; + } + rollback() { if (!this.isAborted) { log(this.router, this.sequence, this.targetName + ': transition was aborted'); diff --git a/tests/async_get_handler_test.ts b/tests/async_get_handler_test.ts index 2b435d7..f51dad7 100644 --- a/tests/async_get_handler_test.ts +++ b/tests/async_get_handler_test.ts @@ -1,11 +1,13 @@ -import { Route } from 'router'; +import Router, { Route, Transition, TransitionError } from 'router'; +import RouteInfo from 'router/route-info'; import { Dict } from 'router/core'; import { Promise } from 'rsvp'; -import { createHandler, TestRouter } from './test_helpers'; +import { createHandler, TestRouter, trigger } from './test_helpers'; -function map(router: TestRouter) { +function map(router: Router) { router.map(function (match) { match('/index').to('index'); + match('/query').to('query'); match('/foo').to('foo', function (match) { match('/').to('fooIndex'); match('/bar').to('fooBar'); @@ -13,6 +15,15 @@ function map(router: TestRouter) { }); } +function createDeferred() { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + // Intentionally use QUnit.module instead of module from test_helpers // so that we avoid using Backburner to handle the async portions of // the test suite @@ -20,7 +31,7 @@ let routes: Dict; let router: TestRouter; QUnit.module('Async Get Handler', { beforeEach: function () { - QUnit.config.testTimeout = 60000; + QUnit.config.testTimeout = 6000; routes = {}; }, @@ -116,3 +127,173 @@ QUnit.test('calls hooks of lazily-resolved routes in order', function (assert) { done(); }, null); }); + +QUnit.test('pause transitions', function (assert) { + let done = assert.async(); + let operations: string[] = []; + let enteredWillChange = 0; + let enteredDidError = 0; + + class PauseRouter extends TestRouter { + getRoute(name: string) { + operations.push('resolved ' + name); + return routes[name] || (routes[name] = createHandler('empty')); + } + } + + let router: Router = new PauseRouter(); + + router.routeWillChange = (transition: Transition) => { + enteredWillChange++; + + const { promise, resolve, reject } = createDeferred(); + transition.waitFor(promise); + setTimeout(() => { + operations.push('paused transition'); + if (enteredWillChange === 1) { + resolve(); + operations.push('resolved pause'); + } else { + reject('reject'); + operations.push('rejected pause'); + } + }, 1); + }; + + router.transitionDidError = (error: TransitionError, transition: Transition) => { + enteredDidError++; + assert.equal('reject', error.error); + transition.trigger(false, 'error', error.error, transition, error.route); + transition.abort(); + return error.error; + }; + + map(router); + + routes.index = createHandler('index', { + model: function () { + operations.push('model index'); + }, + }); + routes.foo = createHandler('foo', { + model: function () { + operations.push('model foo'); + }, + }); + routes.fooBar = createHandler('fooBar', { + model: function () { + operations.push('model fooBar'); + }, + }); + + router.transitionTo('/index').then(function () { + assert.deepEqual( + operations, + [ + 'resolved index', + 'paused transition', + 'resolved pause', + 'model index', + ], + 'order of /index operations is correct' + ); + + operations = []; + + router.transitionTo('/foo/bar').catch(function () { + assert.deepEqual( + operations, + [ + 'resolved foo', + 'resolved fooBar', + 'paused transition', + 'rejected pause', + ], + 'order of /foo/bar operations is correct' + ); + done(); + }); + + }, null); +}); + +QUnit.test('pause transitions query params only', function (assert) { + let done = assert.async(); + let operations: string[] = []; + let enteredWillChange = 0; + + class QpPauseRouter extends TestRouter { + getRoute(name: string) { + operations.push('resolved ' + name); + return routes[name] || (routes[name] = createHandler('empty')); + } + triggerEvent( + handlerInfos: RouteInfo[], + ignoreFailure: boolean, + name: string, + args: any[] + ) { + trigger(handlerInfos, ignoreFailure, name, ...args); + } + } + + let router: Router = new QpPauseRouter(); + + router.routeWillChange = (transition: Transition) => { + enteredWillChange++; + + const { promise, resolve } = createDeferred(); + transition.waitFor(promise); + setTimeout(() => { + operations.push('paused transition'); + resolve(); + operations.push('resolved pause'); + }, 1); + }; + + map(router); + + routes.query = createHandler('query', { + model: function () { + operations.push('model query'); + }, + + events: { + finalizeQueryParamChange: function ({ param }) { + operations.push('param is now ' + param); + } + }, + }); + + router.transitionTo('/query').then(function () { + operations = []; + router.transitionTo('/query?param=1').then(function () { + assert.deepEqual( + operations, + [ + 'resolved query', + 'param is now 1', + 'paused transition', + 'resolved pause', + ], + 'order of /query?param=1 operations is correct' + ); + + operations = []; + router.transitionTo('/query?param=2').then(function () { + assert.deepEqual( + operations, + [ + 'resolved query', + 'param is now 2', + 'paused transition', + 'resolved pause', + ], + 'order of /query?param=2 operations is correct' + ); + done(); + }); + + }, null); + }); +});