From 10d7cab0f90ed874db17b0a0fb3b2109f8476ceb Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:24:09 -0400 Subject: [PATCH] Revert "Pull router_js into the Ember monorepo" --- package.json | 4 +- packages/@ember/-internals/package.json | 2 +- packages/@ember/application/package.json | 2 +- packages/@ember/engine/package.json | 2 +- packages/@ember/routing/package.json | 2 +- packages/ember-testing/package.json | 2 +- packages/ember/package.json | 2 +- packages/internal-test-helpers/package.json | 2 +- packages/router_js/.eslintignore | 1 - packages/router_js/.gitignore | 7 - packages/router_js/ARCHITECTURE.md | 186 - packages/router_js/CHANGELOG.md | 121 - packages/router_js/LICENSE | 19 - packages/router_js/README.md | 5 - packages/router_js/index.ts | 19 - packages/router_js/lib/core.ts | 7 - packages/router_js/lib/route-info.ts | 603 -- packages/router_js/lib/router.ts | 1106 --- .../router_js/lib/transition-aborted-error.ts | 40 - packages/router_js/lib/transition-intent.ts | 16 - .../named-transition-intent.ts | 257 - .../url-transition-intent.ts | 81 - packages/router_js/lib/transition-state.ts | 130 - packages/router_js/lib/transition.ts | 465 -- .../router_js/lib/unrecognized-url-error.ts | 31 - packages/router_js/lib/utils.ts | 177 - packages/router_js/package.json | 33 - .../router_js/tests/async_get_handler_test.ts | 118 - packages/router_js/tests/query_params_test.ts | 637 -- packages/router_js/tests/route_info_test.ts | 236 - packages/router_js/tests/router_test.ts | 6629 ----------------- packages/router_js/tests/test_helpers.ts | 174 - .../tests/transition-aborted-error_test.ts | 38 - .../router_js/tests/transition_intent_test.ts | 306 - .../router_js/tests/transition_state_test.ts | 115 - .../tests/unrecognized-url-error_test.ts | 16 - packages/router_js/tests/utils_test.ts | 32 - pnpm-lock.yaml | 94 +- rollup.config.mjs | 3 +- 39 files changed, 50 insertions(+), 11670 deletions(-) delete mode 100644 packages/router_js/.eslintignore delete mode 100644 packages/router_js/.gitignore delete mode 100644 packages/router_js/ARCHITECTURE.md delete mode 100644 packages/router_js/CHANGELOG.md delete mode 100644 packages/router_js/LICENSE delete mode 100644 packages/router_js/README.md delete mode 100644 packages/router_js/index.ts delete mode 100644 packages/router_js/lib/core.ts delete mode 100644 packages/router_js/lib/route-info.ts delete mode 100644 packages/router_js/lib/router.ts delete mode 100644 packages/router_js/lib/transition-aborted-error.ts delete mode 100644 packages/router_js/lib/transition-intent.ts delete mode 100644 packages/router_js/lib/transition-intent/named-transition-intent.ts delete mode 100644 packages/router_js/lib/transition-intent/url-transition-intent.ts delete mode 100644 packages/router_js/lib/transition-state.ts delete mode 100644 packages/router_js/lib/transition.ts delete mode 100644 packages/router_js/lib/unrecognized-url-error.ts delete mode 100644 packages/router_js/lib/utils.ts delete mode 100644 packages/router_js/package.json delete mode 100644 packages/router_js/tests/async_get_handler_test.ts delete mode 100644 packages/router_js/tests/query_params_test.ts delete mode 100644 packages/router_js/tests/route_info_test.ts delete mode 100644 packages/router_js/tests/router_test.ts delete mode 100644 packages/router_js/tests/test_helpers.ts delete mode 100644 packages/router_js/tests/transition-aborted-error_test.ts delete mode 100644 packages/router_js/tests/transition_intent_test.ts delete mode 100644 packages/router_js/tests/transition_state_test.ts delete mode 100644 packages/router_js/tests/unrecognized-url-error_test.ts delete mode 100644 packages/router_js/tests/utils_test.ts diff --git a/package.json b/package.json index 02c2e2d0c6f..29b608a5bcd 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "ember-router-generator": "^2.0.0", "inflection": "^2.0.1", "route-recognizer": "^0.3.4", + "router_js": "^8.0.6", "semver": "^7.5.2", "silent-error": "^1.1.1", "simple-html-tokenizer": "^0.5.11" @@ -137,7 +138,6 @@ "recast": "^0.22.0", "resolve.exports": "^2.0.3", "rollup": "^4.57.1", - "router_js": "workspace:*", "rsvp": "^4.8.5", "terser": "^5.42.0", "testem": "^3.10.1", @@ -384,4 +384,4 @@ } }, "packageManager": "pnpm@10.30.3" -} +} \ No newline at end of file diff --git a/packages/@ember/-internals/package.json b/packages/@ember/-internals/package.json index cc947bbb6b5..6f6d3bdc2ec 100644 --- a/packages/@ember/-internals/package.json +++ b/packages/@ember/-internals/package.json @@ -69,7 +69,7 @@ "ember-template-compiler": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*", + "router_js": "^8.0.6", "rsvp": "^4.8.5" }, "devDependencies": { diff --git a/packages/@ember/application/package.json b/packages/@ember/application/package.json index 074819001a0..ba569f437f9 100644 --- a/packages/@ember/application/package.json +++ b/packages/@ember/application/package.json @@ -30,6 +30,6 @@ "ember-template-compiler": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*" + "router_js": "^8.0.6" } } diff --git a/packages/@ember/engine/package.json b/packages/@ember/engine/package.json index b08914eb32f..0edd61dc7be 100644 --- a/packages/@ember/engine/package.json +++ b/packages/@ember/engine/package.json @@ -27,6 +27,6 @@ "dag-map": "^2.0.2", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*" + "router_js": "^8.0.6" } } diff --git a/packages/@ember/routing/package.json b/packages/@ember/routing/package.json index b75af126221..0ef578d40dc 100644 --- a/packages/@ember/routing/package.json +++ b/packages/@ember/routing/package.json @@ -37,6 +37,6 @@ "dag-map": "^2.0.2", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*" + "router_js": "^8.0.6" } } diff --git a/packages/ember-testing/package.json b/packages/ember-testing/package.json index d2ceb6b5435..e340f1de93b 100644 --- a/packages/ember-testing/package.json +++ b/packages/ember-testing/package.json @@ -27,6 +27,6 @@ "backburner.js": "^2.7.0", "ember": "workspace:*", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*" + "router_js": "^8.0.6" } } diff --git a/packages/ember/package.json b/packages/ember/package.json index 5fd19c1e9c5..9f3ebd2e8c2 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -46,7 +46,7 @@ "ember-testing": "workspace:*", "expect-type": "^0.15.0", "internal-test-helpers": "workspace:*", - "router_js": "workspace:*", + "router_js": "^8.0.6", "rsvp": "^4.8.5" } } diff --git a/packages/internal-test-helpers/package.json b/packages/internal-test-helpers/package.json index d09f8ef17c0..84583e0aaf8 100644 --- a/packages/internal-test-helpers/package.json +++ b/packages/internal-test-helpers/package.json @@ -39,7 +39,7 @@ "dag-map": "^2.0.2", "ember": "workspace:*", "ember-template-compiler": "workspace:*", - "router_js": "workspace:*", + "router_js": "^8.0.6", "rsvp": "^4.8.5", "simple-html-tokenizer": "^0.5.11" } diff --git a/packages/router_js/.eslintignore b/packages/router_js/.eslintignore deleted file mode 100644 index 849ddff3b7e..00000000000 --- a/packages/router_js/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/packages/router_js/.gitignore b/packages/router_js/.gitignore deleted file mode 100644 index bf5d524735e..00000000000 --- a/packages/router_js/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -/.bundle -/dist -/tmp -/node_modules - -dist/tests -.eslintcache diff --git a/packages/router_js/ARCHITECTURE.md b/packages/router_js/ARCHITECTURE.md deleted file mode 100644 index a1e6391d45e..00000000000 --- a/packages/router_js/ARCHITECTURE.md +++ /dev/null @@ -1,186 +0,0 @@ -# [router.js](https://github.com/tildeio/router.js) Architecture - -This is a guide to `router.js`'s internals. - -`router.js` is a stand-alone microlibrary for client-side routing in JavaScript -applications. It's notably used by the [Ember.js Router][Ember Router]. - -## Scope of `router.js` and its Dependencies - -Ember.js's router consumes `router.js`, which in turn consumes -[route-recognizer](https://github.com/tildeio/route-recognizer). - -The division of responsibilities of these three libs is as follows: - -### `route-recognizer` - -`route-recognizer` is an engine for both parsing/generating URLs into/from -parameters. - -It can take a URL like `articles/123/comments` and parse out the parameter -`{ article_id: "123" }`. - -It can take `{ article_id: "123" }` and a route descriptor like -`articles/:article_id/comments` and generate `articles/123/comments`. - -### `router.js` - -`router.js` adds the concept of transitions to `route-recognizer`'s -URL parsing engine. - -Transitions can be URL-initiated (via browser navigation) or can be -directly initiated via route name -(e.g. `transitionTo('articles', articleObject)`). - -`router.js` resolves all of the model objects that needed to be loaded -in order to enter a route. - -e.g. to navigate to `articles/123/comments/2`, a promise for both the -`article` and `comments` routes need to be fulfilled. - -### Ember Router - -The [Ember Router][] adds a DSL for declaring your app's routes on top of -`router.js`. It defines the API for the `Ember.Route` class that handles -intelligent defaults, rendering templates, and loading data into controllers. - -## History - -`router.js` has gone through a few iterations between 2013 and 2014: - -- July of 2013 – `router.js` adds promise-awareness. -- Jan 2014 – refactored `router.js`'s primitives to handle corner cases. - -### Corner Cases - -1. Avoid running `model` hooks (responsible for fetching data needed to enter a - route) for shared parent routes. - -2. Avoid running model hooks when redirecting in the middle of another transition. - e.g. during a transition to `articles/123/comments/2` you redirect to - `articles/123/comments/3` after resolving Article 123 and you want to - avoid re-running the hooks to load Article 123 again. - -3. Handle two different approaches to transitions: - - URL based (where a URL is parsed into route parameters that are used to - load all the data needed to enter a route (e.g. `{ article_id: 123 }`). - - - direct named transition-based, where a route name and any context objects - are provided (e.g. `transitionTo('article', articleObject)`), and the - provided context object(s) might be promises that can't be serialized - into URL params until they've fulfilled. - -## Classes - -### `HandlerInfo` - -A `HandlerInfo` is an object that describes the state of a route handler. - -For example, the `foo/bar` URL likely breaks down into a hierarchy of two -handlers: the `foo` handler and the `bar` handler. A "handler" is just an -object that defines hooks that `router.js` will call in the course of a -transition (e.g. `model`, `beforeModel`, `setup`, etc.). - -In Ember.js, handlers are instances of `Ember.Route`. - -A `HandlerInfo` instance contains that handler's model (e.g. `articleObject`), -or the URL parameters associated with the current state of that handler -(e.g. `{ article_id: '123' }`). - -Because `router.js` allows you to reuse handlers between different routes and -route hierarchies, we need `HandlerInfo`s to describe the state of each route -hierarchy. - -`HandlerInfo` is a top-level class with 3 subclasses: - -#### `UnresolvedHandlerInfoByParam` - -`UnresolvedHandlerInfoByParam` has the URL params stored on it which it can use -to resolve itself (by calling the handler's `beforeModel`/`model`/`afterModel` -hooks). - -#### `UnresolvedHandlerInfoByObject` - -`UnresolvedHandlerInfoByObject` has a context object, but no URL params. -It can use the context to resolve itself and serialize into URL params once -the context object is fulfilled. - -#### `ResolvedHandlerInfo` - -`ResolvedHandlerInfo` has calculated its URL params and resolved context/model -object. - -#### Public API - -`HandlerInfo` has just a `resolve` method which fires all `model` hooks and -ultimately resolves to a `ResolvedHandlerInfo` object. - -The `ResolvedHandlerInfo`'s `resolve` method just returns a promise that -fulfills with itself. - -### `TransitionState` - -The `TransitionState` object consists of an array of `HandlerInfo`s -(though more might be added to it; not sure yet). - -#### Public API - -It too has a public API consisting only of a `resolve` method that -will loop through all of its `HandlerInfo`s, swapping unresolved -`HandlerInfo`s with `ResolvedHandlerInfo`s as it goes. - -Instances of `Router` and `Transition` contain `TransitionState` -properties, which is useful since, depending on whether or not there is -a currently active transition, the "starting point" of a transition -might be the router's current hierarchy of `ResolvedHandlerInfo`s, or it -might be a transition's hierarchy of `ResolvedHandlerInfo`s mixed with -unresolved HandlerInfos. - -### `TransitionIntent` - -A `TransitionIntent` describes an attempt to transition. - -via URL -or by named transition (via its subclasses `URLTransitionIntent` and -`NamedTransitionIntent`). - -#### `URLTransitionIntent` - -A `URLTransitionIntent` has a `url` property. - -#### `NamedTransitionIntent` - -A `NamedTransitionIntent` has a target route `name` and `contexts` array -property. - -This class defines only one method `applyToState` which takes an instance of -`TransitionState` and plays this `TransitionIntent` on top of it to generate -and return a new instance of `TransitionState` that contains a combination of -resolved and unresolved `HandlerInfo`s. - -`TransitionIntent`s don't care whether the provided state comes from a router -or a currently active transition; whatever you provide it, both subclasses of -`TransitionIntent`s are smart enough to spit out a `TransitionState` -containing `HandlerInfo`s that still need to be resolved in order to complete -a transition. - -Much of the messy logic that used to live in `paramsForHandler`/`getMatchPoint` -now live way less messily in the `applyToState` methods. - -This makes it easy to detect corner cases like no-op transitions – if the -returned `TransitionState` consists entirely of `ResolvedHandlerInfo`s, there's -no need to fire off a transition. It simplifies things like redirecting into a -child route without winding up in some infinite loop on the parent route hook -that's doing the redirecting. - -This simplifies `Transition#retry`; to retry a transition, provide its `intent` -property to the transitioning function used by `transitionTo`, `handleURL`. -`handle` function will make the right choice as to the correct `TransitionState` -to pass to the intent's `applyToState` method. - -This approach is used to implement `Router#isActive`. You can determine if a -destination route is active by constructing a `TransitionIntent`, applying it -to the router's current state, and returning `true` if all of the -`HandlerInfo`s are already resolved. - -[Ember Router]: http://emberjs.com/guides/routing/ diff --git a/packages/router_js/CHANGELOG.md b/packages/router_js/CHANGELOG.md deleted file mode 100644 index 35c514e5a56..00000000000 --- a/packages/router_js/CHANGELOG.md +++ /dev/null @@ -1,121 +0,0 @@ -## v8.0.6 (2024-08-02) - -- [#335] Fix followRedirects when source is async and destination is sync ([@davidtaylorhq](https://github.com/davidtaylorhq)) - - -## v8.0.5 (2024-03-19) - -- [#339] Fix a type error when exactOptionalPropertyTypes is enabled ([@boris-petrov](https://github.com/boris-petrov)) - - -## v8.0.4 (2024-03-06) - -- [#336] Calling recognize should not affect the transition.from query params for subsequent transitions ([@chbonser](https://github.com/chbonser)) - - -## v8.0.3 (2022-08-27) - -- [#334] Fix undefined routeInfo in routeInfo's `find` callback ([@sly7-7](https://github.com/sly7-7)) - - -## v8.0.2 (2022-02-09) - -- [#332] Correct more incorrect TypeScript types - -## v8.0.1 (2022-02-03) - -- [#331] Correct some incorrect TypeScript types - -## v8.0.0 (2022-02-02) - -#### :boom: Breaking Change - -- [#329](https://github.com/tildeio/router.js/pull/329) Better Types ([@wagenet](https://github.com/wagenet)) - -#### Committers: 1 - -- Peter Wagenet ([@wagenet](https://github.com/wagenet)) - -## v7.3.0 (2021-03-07) - -#### :rocket: Enhancement - -- [#321](https://github.com/tildeio/router.js/pull/321) Add `isIntermediate` flag to Transition ([@sly7-7](https://github.com/sly7-7)) - -#### :house: Internal - -- [#320](https://github.com/tildeio/router.js/pull/320) Remove testing for multiple platforms. ([@rwjblue](https://github.com/rwjblue)) - -#### Committers: 2 - -- Robert Jackson ([@rwjblue](https://github.com/rwjblue)) -- Sylvain MINA ([@sly7-7](https://github.com/sly7-7)) - -## v7.2.0 (2021-03-07) - -#### :bug: Bug Fix - -- [#319](https://github.com/tildeio/router.js/pull/319) Ensure query params are preserved through an intermediate loading state transition ([@sly7-7](https://github.com/sly7-7)) - -#### :memo: Documentation - -- [#316](https://github.com/tildeio/router.js/pull/316) Publish type declaration ([@xg-wang](https://github.com/xg-wang)) - -#### :house: Internal - -- [#318](https://github.com/tildeio/router.js/pull/318) add livereload so tests reload when i make changes ([@stefanpenner](https://github.com/stefanpenner)) -- [#309](https://github.com/tildeio/router.js/pull/309) Refactor TransitionAbort to builder interface ([@rwjblue](https://github.com/rwjblue)) -- [#306](https://github.com/tildeio/router.js/pull/306) Simplify TransitionState resolution system. ([@rwjblue](https://github.com/rwjblue)) -- [#314](https://github.com/tildeio/router.js/pull/314) [Closes [#313](https://github.com/tildeio/router.js/issues/313)] Fix Typo shouldSupercede -> shouldSupersede ([@stefanpenner](https://github.com/stefanpenner)) -- [#315](https://github.com/tildeio/router.js/pull/315) Fix other typo’s ([@stefanpenner](https://github.com/stefanpenner)) -- [#312](https://github.com/tildeio/router.js/pull/312) Upgrade `devDependencies` ([@stefanpenner](https://github.com/stefanpenner)) -- [#311](https://github.com/tildeio/router.js/pull/311) Upgrade CI ([@stefanpenner](https://github.com/stefanpenner)) - -#### Committers: 4 - -- Robert Jackson ([@rwjblue](https://github.com/rwjblue)) -- Stefan Penner ([@stefanpenner](https://github.com/stefanpenner)) -- Sylvain MINA ([@sly7-7](https://github.com/sly7-7)) -- Thomas Wang ([@xg-wang](https://github.com/xg-wang)) - -## v7.1.1 (2020-11-06) - -#### :bug: Bug Fix - -- [#308](https://github.com/tildeio/router.js/pull/308) Provide transition to `setupContext` for internal transition ([@rreckonerr](https://github.com/rreckonerr)) - -#### Committers: 1 - -- Volodymyr Radchenko ([@rreckonerr](https://github.com/rreckonerr)) - -## v7.1.0 (2020-09-09) - -#### :rocket: Enhancement - -- [#305](https://github.com/tildeio/router.js/pull/305) Add better Transition debugging information. ([@rwjblue](https://github.com/rwjblue)) - -#### Committers: 1 - -- Robert Jackson ([@rwjblue](https://github.com/rwjblue)) - -## v7.0.0 (2020-07-21) - -#### :boom: Breaking Change - -- [#297](https://github.com/tildeio/router.js/pull/297) Update TypeScript to 3.9 ([@xg-wang](https://github.com/xg-wang)) -- [#294](https://github.com/tildeio/router.js/pull/294) Drop Node < 10. ([@rwjblue](https://github.com/rwjblue)) -- [#289](https://github.com/tildeio/router.js/pull/289) Upgrade TypeScript to 3.5 ([@xg-wang](https://github.com/xg-wang)) - -#### :house: Internal - -- [#301](https://github.com/tildeio/router.js/pull/301) Add automated release setup. ([@rwjblue](https://github.com/rwjblue)) -- [#300](https://github.com/tildeio/router.js/pull/300) Update Babel to latest. ([@rwjblue](https://github.com/rwjblue)) -- [#299](https://github.com/tildeio/router.js/pull/299) Update remaining dependencies/devDependencies to latest. ([@rwjblue](https://github.com/rwjblue)) -- [#298](https://github.com/tildeio/router.js/pull/298) Update prettier to 2.0.5. ([@rwjblue](https://github.com/rwjblue)) -- [#296](https://github.com/tildeio/router.js/pull/296) Migrate from TSLint to ESLint ([@rwjblue](https://github.com/rwjblue)) -- [#295](https://github.com/tildeio/router.js/pull/295) Add GitHub Actions CI setup ([@rwjblue](https://github.com/rwjblue)) - -#### Committers: 2 - -- Robert Jackson ([@rwjblue](https://github.com/rwjblue)) -- Thomas Wang ([@xg-wang](https://github.com/xg-wang)) diff --git a/packages/router_js/LICENSE b/packages/router_js/LICENSE deleted file mode 100644 index 13883e15041..00000000000 --- a/packages/router_js/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2014 Yehuda Katz, Tom Dale, and contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE diff --git a/packages/router_js/README.md b/packages/router_js/README.md deleted file mode 100644 index 82c360744a5..00000000000 --- a/packages/router_js/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# router.js - -This package was a dependency of Ember.js and was migrated into the monorepo from https://github.com/emberjs/router.js - -`router.js` was always bundled in the ember-source package and was never consumed directly as a package. This has been merged into the monorepo to prepare for the upcoming [Route Manager RFC](https://github.com/emberjs/rfcs/pull/1169) to make it easier to move things around during the implementation phase. diff --git a/packages/router_js/index.ts b/packages/router_js/index.ts deleted file mode 100644 index 11f8f8bb438..00000000000 --- a/packages/router_js/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { default } from './lib/router'; -export { - default as InternalTransition, - logAbort, - STATE_SYMBOL, - PARAMS_SYMBOL, - QUERY_PARAMS_SYMBOL, -} from './lib/transition'; - -export type { PublicTransition as Transition } from './lib/transition'; - -export { default as TransitionState, TransitionError } from './lib/transition-state'; -export { - default as InternalRouteInfo, - type Route, - type RouteInfo, - type RouteInfoWithAttributes, - type ModelFor, -} from './lib/route-info'; diff --git a/packages/router_js/lib/core.ts b/packages/router_js/lib/core.ts deleted file mode 100644 index cc35265994a..00000000000 --- a/packages/router_js/lib/core.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type Present = object | void; -export type Option = T | null; -export type Maybe = Option | undefined | void; -export type Recast = (T & U) | U; -export interface Dict { - [key: string]: T; -} diff --git a/packages/router_js/lib/route-info.ts b/packages/router_js/lib/route-info.ts deleted file mode 100644 index ecd4875b1a8..00000000000 --- a/packages/router_js/lib/route-info.ts +++ /dev/null @@ -1,603 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -import { Promise } from 'rsvp'; -import type { Dict, Option } from './core'; -import type { SerializerFunc } from './router'; -import type Router from './router'; -import type { PublicTransition as Transition } from './transition'; -import type InternalTransition from './transition'; -import { isTransition, PARAMS_SYMBOL, prepareResult, QUERY_PARAMS_SYMBOL } from './transition'; -import { isParam, isPromise, merge } from './utils'; -import { throwIfAborted } from './transition-aborted-error'; - -export type IModel = {} & { - id?: string | number; -}; - -export type ModelFor = T extends Route ? V : never; - -export interface Route { - inaccessibleByURL?: boolean; - routeName: string; - _internalName: string; - context: T | undefined; - events?: Dict<(...args: unknown[]) => unknown>; - model?(params: Dict, transition: Transition): PromiseLike | undefined | T; - deserialize?(params: Dict, transition: Transition): T | PromiseLike | undefined; - serialize?(model: T | undefined, params: string[]): Dict | undefined; - beforeModel?(transition: Transition): PromiseLike | any; - afterModel?(resolvedModel: T | undefined, transition: Transition): PromiseLike | any; - setup?(context: T | undefined, transition: Transition): void; - enter?(transition: Transition): void; - exit?(transition?: Transition): void; - _internalReset?(wasReset: boolean, transition?: Transition): void; - contextDidChange?(): void; - redirect?(context: T | undefined, transition: Transition): void; - buildRouteInfoMetadata?(): unknown; -} - -export interface RouteInfo { - readonly name: string; - readonly parent: RouteInfo | RouteInfoWithAttributes | null; - readonly child: RouteInfo | RouteInfoWithAttributes | null; - readonly localName: string; - readonly params: Dict | undefined; - readonly paramNames: string[]; - readonly queryParams: Dict; - readonly metadata: unknown; - find( - predicate: (this: any, routeInfo: RouteInfo, i: number) => boolean, - thisArg?: any - ): RouteInfo | undefined; -} - -export interface RouteInfoWithAttributes extends RouteInfo { - attributes: any; -} - -type RouteInfosKey = InternalRouteInfo; - -let ROUTE_INFOS = new WeakMap(); - -export function toReadOnlyRouteInfo( - routeInfos: InternalRouteInfo[], - queryParams: Dict = {}, - options: { - includeAttributes?: boolean; - localizeMapUpdates?: boolean; - } = { includeAttributes: false, localizeMapUpdates: false } -): RouteInfoWithAttributes[] | RouteInfo[] { - const LOCAL_ROUTE_INFOS = new WeakMap(); - - return routeInfos.map((info, i) => { - let { name, params, paramNames, context, route } = info; - // SAFETY: This should be safe since it is just for use as a key - let key = info as unknown as RouteInfosKey; - if (ROUTE_INFOS.has(key) && options.includeAttributes) { - let routeInfo = ROUTE_INFOS.get(key)!; - routeInfo = attachMetadata(route!, routeInfo); - let routeInfoWithAttribute = createRouteInfoWithAttributes(routeInfo, context); - LOCAL_ROUTE_INFOS.set(key, routeInfo); - if (!options.localizeMapUpdates) { - ROUTE_INFOS.set(key, routeInfoWithAttribute); - } - return routeInfoWithAttribute as RouteInfoWithAttributes; - } - - const routeInfosRef = options.localizeMapUpdates ? LOCAL_ROUTE_INFOS : ROUTE_INFOS; - - let routeInfo: RouteInfo = { - find( - predicate: (this: any, routeInfo: RouteInfo, i: number, arr?: RouteInfo[]) => boolean, - thisArg: any - ) { - let publicInfo; - let arr: RouteInfo[] = []; - - if (predicate.length === 3) { - arr = routeInfos.map( - // SAFETY: This should be safe since it is just for use as a key - (info) => routeInfosRef.get(info as unknown as RouteInfosKey)! - ); - } - - for (let i = 0; routeInfos.length > i; i++) { - // SAFETY: This should be safe since it is just for use as a key - publicInfo = routeInfosRef.get(routeInfos[i] as unknown as RouteInfosKey)!; - if (predicate.call(thisArg, publicInfo, i, arr)) { - return publicInfo; - } - } - - return undefined; - }, - - get name() { - return name; - }, - - get paramNames() { - return paramNames; - }, - - get metadata() { - return buildRouteInfoMetadata(info.route); - }, - - get parent() { - let parent = routeInfos[i - 1]; - - if (parent === undefined) { - return null; - } - - // SAFETY: This should be safe since it is just for use as a key - return routeInfosRef.get(parent as unknown as RouteInfosKey)!; - }, - - get child() { - let child = routeInfos[i + 1]; - - if (child === undefined) { - return null; - } - - // SAFETY: This should be safe since it is just for use as a key - return routeInfosRef.get(child as unknown as RouteInfosKey)!; - }, - - get localName() { - let parts = this.name.split('.'); - return parts[parts.length - 1]!; - }, - - get params() { - return params; - }, - - get queryParams() { - return queryParams; - }, - }; - - if (options.includeAttributes) { - routeInfo = createRouteInfoWithAttributes(routeInfo, context); - } - - // SAFETY: This should be safe since it is just for use as a key - LOCAL_ROUTE_INFOS.set(info as unknown as RouteInfosKey, routeInfo); - - if (!options.localizeMapUpdates) { - // SAFETY: This should be safe since it is just for use as a key - ROUTE_INFOS.set(info as unknown as RouteInfosKey, routeInfo); - } - - return routeInfo; - }); -} - -function createRouteInfoWithAttributes( - routeInfo: RouteInfo, - context: any -): RouteInfoWithAttributes { - let attributes = { - get attributes() { - return context; - }, - }; - - if (!Object.isExtensible(routeInfo) || routeInfo.hasOwnProperty('attributes')) { - return Object.freeze(Object.assign({}, routeInfo, attributes)); - } - - return Object.assign(routeInfo, attributes); -} - -function buildRouteInfoMetadata(route?: Route) { - if (route !== undefined && route !== null && route.buildRouteInfoMetadata !== undefined) { - return route.buildRouteInfoMetadata(); - } - - return null; -} - -function attachMetadata(route: Route, routeInfo: RouteInfo) { - let metadata = { - get metadata() { - return buildRouteInfoMetadata(route); - }, - }; - - if (!Object.isExtensible(routeInfo) || routeInfo.hasOwnProperty('metadata')) { - return Object.freeze(Object.assign({}, routeInfo, metadata)); - } - - return Object.assign(routeInfo, metadata); -} - -export default class InternalRouteInfo { - private _routePromise?: Promise = undefined; - private _route?: Option = null; - protected router: Router; - declare paramNames: string[]; - declare name: string; - params: Dict | undefined = {}; - declare queryParams?: Dict; - declare context?: ModelFor | PromiseLike> | undefined; - isResolved = false; - - constructor(router: Router, name: string, paramNames: string[], route?: R) { - this.name = name; - this.paramNames = paramNames; - this.router = router; - if (route) { - this._processRoute(route); - } - } - - getModel(_transition: InternalTransition) { - return Promise.resolve(this.context); - } - - serialize(_context?: ModelFor | null): Dict | undefined { - return this.params || {}; - } - - resolve(transition: InternalTransition): Promise> { - return Promise.resolve(this.routePromise) - .then((route: Route) => { - throwIfAborted(transition); - return route; - }) - .then(() => this.runBeforeModelHook(transition)) - .then(() => throwIfAborted(transition)) - .then(() => this.getModel(transition)) - .then((resolvedModel) => { - throwIfAborted(transition); - return resolvedModel; - }) - .then((resolvedModel) => this.runAfterModelHook(transition, resolvedModel)) - .then((resolvedModel) => this.becomeResolved(transition, resolvedModel)); - } - - becomeResolved( - transition: InternalTransition | null, - resolvedContext: ModelFor | undefined - ): ResolvedRouteInfo { - let params = this.serialize(resolvedContext); - - if (transition) { - this.stashResolvedModel(transition, resolvedContext); - transition[PARAMS_SYMBOL] = transition[PARAMS_SYMBOL] || {}; - transition[PARAMS_SYMBOL][this.name] = params; - } - - let context; - let contextsMatch = resolvedContext === this.context; - - if ('context' in this || !contextsMatch) { - context = resolvedContext; - } - - // SAFETY: Since this is just for lookup, it should be safe - let cached = ROUTE_INFOS.get(this as unknown as InternalRouteInfo); - let resolved = new ResolvedRouteInfo( - this.router, - this.name, - this.paramNames, - params, - this.route!, - context - ); - - if (cached !== undefined) { - // SAFETY: This is potentially a bit risker, but for what we're doing, it should be ok. - ROUTE_INFOS.set(resolved as unknown as InternalRouteInfo, cached); - } - - return resolved; - } - - shouldSupersede(routeInfo?: InternalRouteInfo) { - // Prefer this newer routeInfo over `other` if: - // 1) The other one doesn't exist - // 2) The names don't match - // 3) This route has a context that doesn't match - // the other one (or the other one doesn't have one). - // 4) This route has parameters that don't match the other. - if (!routeInfo) { - return true; - } - - let contextsMatch = routeInfo.context === this.context; - return ( - routeInfo.name !== this.name || - ('context' in this && !contextsMatch) || - (this.hasOwnProperty('params') && !paramsMatch(this.params, routeInfo.params)) - ); - } - - get route(): R | undefined { - // _route could be set to either a route object or undefined, so we - // compare against null to know when it's been set - if (this._route !== null) { - return this._route; - } - - return this.fetchRoute(); - } - - set route(route: R | undefined) { - this._route = route; - } - - get routePromise(): Promise { - if (this._routePromise) { - return this._routePromise; - } - - this.fetchRoute(); - - return this._routePromise!; - } - - set routePromise(routePromise: Promise) { - this._routePromise = routePromise; - } - - protected log(transition: InternalTransition, message: string) { - if (transition.log) { - transition.log(this.name + ': ' + message); - } - } - - private updateRoute(route: R) { - route._internalName = this.name; - return (this.route = route); - } - - private runBeforeModelHook(transition: InternalTransition) { - if (transition.trigger) { - transition.trigger(true, 'willResolveModel', transition, this.route); - } - - let result; - if (this.route) { - if (this.route.beforeModel !== undefined) { - result = this.route.beforeModel(transition); - } - } - - if (isTransition(result)) { - result = null; - } - - return Promise.resolve(result); - } - - private runAfterModelHook( - transition: InternalTransition, - resolvedModel?: ModelFor | null - ): Promise> { - // Stash the resolved model on the payload. - // This makes it possible for users to swap out - // the resolved model in afterModel. - let name = this.name; - this.stashResolvedModel(transition, resolvedModel!); - - let result; - if (this.route !== undefined) { - if (this.route.afterModel !== undefined) { - result = this.route.afterModel(resolvedModel!, transition); - } - } - - result = prepareResult(result); - - return Promise.resolve(result).then(() => { - // Ignore the fulfilled value returned from afterModel. - // Return the value stashed in resolvedModels, which - // might have been swapped out in afterModel. - // SAFTEY: We expect this to be of type T, though typing it as such is challenging. - return transition.resolvedModels[name]! as unknown as ModelFor; - }); - } - - private stashResolvedModel( - transition: InternalTransition, - resolvedModel: ModelFor | undefined - ) { - transition.resolvedModels = transition.resolvedModels || {}; - // SAFETY: It's unfortunate that we have to do this cast. It should be safe though. - transition.resolvedModels[this.name] = resolvedModel; - } - - private fetchRoute() { - let route = this.router.getRoute(this.name); - return this._processRoute(route); - } - - private _processRoute(route: R | Promise) { - // Setup a routePromise so that we can wait for asynchronously loaded routes - this.routePromise = Promise.resolve(route); - - // Wait until the 'route' property has been updated when chaining to a route - // that is a promise - if (isPromise(route)) { - this.routePromise = this.routePromise.then((r) => { - return this.updateRoute(r); - }); - // set to undefined to avoid recursive loop in the route getter - return (this.route = undefined); - } else if (route) { - return this.updateRoute(route); - } - - return undefined; - } -} - -export class ResolvedRouteInfo extends InternalRouteInfo { - isResolved: boolean; - context: ModelFor | undefined; - constructor( - router: Router, - name: string, - paramNames: string[], - params: Dict | undefined, - route: R, - context?: ModelFor - ) { - super(router, name, paramNames, route); - this.params = params; - this.isResolved = true; - this.context = context; - } - - resolve(transition: InternalTransition): Promise { - // A ResolvedRouteInfo just resolved with itself. - if (transition && transition.resolvedModels) { - transition.resolvedModels[this.name] = this.context; - } - return Promise.resolve(this); - } -} - -export class UnresolvedRouteInfoByParam extends InternalRouteInfo { - params: Dict = {}; - constructor( - router: Router, - name: string, - paramNames: string[], - params: Dict | undefined, - route?: R - ) { - super(router, name, paramNames, route); - if (params) { - this.params = params; - } - } - - getModel(transition: InternalTransition): Promise> { - let fullParams = this.params; - if (transition && transition[QUERY_PARAMS_SYMBOL]) { - fullParams = {}; - merge(fullParams, this.params); - fullParams['queryParams'] = transition[QUERY_PARAMS_SYMBOL]; - } - - let route = this.route!; - - let result: ModelFor | PromiseLike> | undefined; - - // FIXME: Review these casts - if (route.deserialize) { - result = route.deserialize(fullParams, transition) as - | ModelFor - | PromiseLike> - | undefined; - } else if (route.model) { - result = route.model(fullParams, transition) as - | ModelFor - | PromiseLike> - | undefined; - } - - if (result && isTransition(result)) { - result = undefined; - } - - return Promise.resolve(result); - } -} - -export class UnresolvedRouteInfoByObject extends InternalRouteInfo { - serializer?: SerializerFunc>; - constructor( - router: Router, - name: string, - paramNames: string[], - context: ModelFor | PromiseLike> | undefined - ) { - super(router, name, paramNames); - this.context = context; - this.serializer = this.router.getSerializer(name); - } - - getModel(transition: InternalTransition) { - if (this.router.log !== undefined) { - this.router.log(this.name + ': resolving provided model'); - } - return super.getModel(transition); - } - - /** - @private - - Serializes a route using its custom `serialize` method or - by a default that looks up the expected property name from - the dynamic segment. - - @param {Object} model the model to be serialized for this route - */ - serialize(model?: ModelFor): Dict | undefined { - let { paramNames, context } = this; - - if (!model) { - // SAFETY: By the time we serialize, we expect to be resolved. - // This may not be an entirely safe assumption though no tests fail. - model = context as ModelFor; - } - - let object: Dict = {}; - if (isParam(model)) { - object[paramNames[0]!] = model; - return object; - } - - // Use custom serialize if it exists. - if (this.serializer) { - // invoke this.serializer unbound (getSerializer returns a stateless function) - return this.serializer.call(null, model, paramNames); - } else if (this.route !== undefined) { - if (this.route.serialize) { - return this.route.serialize(model, paramNames); - } - } - - if (paramNames.length !== 1) { - return; - } - - let name = paramNames[0]!; - - if (/_id$/.test(name)) { - // SAFETY: Model is supposed to extend IModel already - object[name] = (model as IModel).id; - } else { - object[name] = model; - } - return object; - } -} - -function paramsMatch(a: Dict | undefined, b: Dict | undefined) { - if (a === b) { - // Both are identical, may both be undefined - return true; - } - - if (!a || !b) { - // Only one is undefined, already checked they aren't identical - return false; - } - - // Note: this assumes that both params have the same - // number of keys, but since we're comparing the - // same routes, they should. - for (let k in a) { - if (a.hasOwnProperty(k) && a[k] !== b[k]) { - return false; - } - } - return true; -} diff --git a/packages/router_js/lib/router.ts b/packages/router_js/lib/router.ts deleted file mode 100644 index 0dd1300ae8c..00000000000 --- a/packages/router_js/lib/router.ts +++ /dev/null @@ -1,1106 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -import type { MatchCallback, Params, QueryParams } from 'route-recognizer'; -import RouteRecognizer from 'route-recognizer'; -import { Promise } from 'rsvp'; -import type { Dict, Maybe, Option } from './core'; -import type { ModelFor, Route, RouteInfo, RouteInfoWithAttributes } from './route-info'; -import type InternalRouteInfo from './route-info'; -import { toReadOnlyRouteInfo } from './route-info'; -import type { OpaqueTransition, PublicTransition as Transition } from './transition'; -import InternalTransition, { logAbort, QUERY_PARAMS_SYMBOL, STATE_SYMBOL } from './transition'; -import { throwIfAborted, isTransitionAborted } from './transition-aborted-error'; -import type { TransitionIntent } from './transition-intent'; -import NamedTransitionIntent from './transition-intent/named-transition-intent'; -import URLTransitionIntent from './transition-intent/url-transition-intent'; -import type { TransitionError } from './transition-state'; -import TransitionState from './transition-state'; -import type { ChangeList, ModelsAndQueryParams } from './utils'; -import { extractQueryParams, forEach, getChangelist, log, merge, promiseLabel } from './utils'; - -export interface SerializerFunc { - (model: T, params: string[]): Dict; -} - -export interface ParsedHandler { - handler: string; - names: string[]; -} - -export default abstract class Router { - private _lastQueryParams = {}; - log?: (message: string) => void; - state?: TransitionState = undefined; - oldState: Maybe> = undefined; - activeTransition?: InternalTransition = undefined; - currentRouteInfos?: InternalRouteInfo[] = undefined; - _changedQueryParams?: Dict = undefined; - currentSequence = 0; - recognizer: RouteRecognizer; - - constructor(logger?: (message: string) => void) { - this.log = logger; - this.recognizer = new RouteRecognizer(); - this.reset(); - } - - abstract getRoute(name: string): R | Promise; - abstract getSerializer(name: string): SerializerFunc> | undefined; - abstract updateURL(url: string): void; - abstract replaceURL(url: string): void; - abstract willTransition( - oldRouteInfos: InternalRouteInfo[], - newRouteInfos: InternalRouteInfo[], - transition: Transition - ): void; - abstract didTransition(routeInfos: InternalRouteInfo[]): void; - abstract triggerEvent( - routeInfos: InternalRouteInfo[], - ignoreFailure: boolean, - name: string, - args: unknown[] - ): void; - - abstract routeWillChange(transition: Transition): void; - abstract routeDidChange(transition: Transition): void; - abstract transitionDidError(error: TransitionError, transition: Transition): Transition | Error; - - /** - The main entry point into the router. The API is essentially - the same as the `map` method in `route-recognizer`. - - This method extracts the String handler at the last `.to()` - call and uses it as the name of the whole route. - - @param {Function} callback - */ - map(callback: MatchCallback) { - this.recognizer.map(callback, function (recognizer, routes) { - for (let i = routes.length - 1, proceed = true; i >= 0 && proceed; --i) { - let route = routes[i]!; - let handler = route.handler as string; - recognizer.add(routes, { as: handler }); - proceed = route.path === '/' || route.path === '' || handler.slice(-6) === '.index'; - } - }); - } - - hasRoute(route: string) { - return this.recognizer.hasRoute(route); - } - - queryParamsTransition( - changelist: ChangeList, - wasTransitioning: boolean, - oldState: TransitionState, - newState: TransitionState - ): OpaqueTransition { - this.fireQueryParamDidChange(newState, changelist); - - if (!wasTransitioning && this.activeTransition) { - // One of the routes in queryParamsDidChange - // caused a transition. Just return that transition. - return this.activeTransition; - } else { - // Running queryParamsDidChange didn't change anything. - // Just update query params and be on our way. - - // We have to return a noop transition that will - // perform a URL update at the end. This gives - // the user the ability to set the url update - // method (default is replaceState). - let newTransition = new InternalTransition(this, undefined, undefined); - newTransition.queryParamsOnly = true; - - oldState.queryParams = this.finalizeQueryParamChange( - newState.routeInfos, - newState.queryParams, - newTransition - ); - - newTransition[QUERY_PARAMS_SYMBOL] = newState.queryParams; - - this.toReadOnlyInfos(newTransition, newState); - - this.routeWillChange(newTransition); - - newTransition.promise = newTransition.promise!.then( - (result: TransitionState | Route | Error | undefined) => { - if (!newTransition.isAborted) { - this._updateURL(newTransition, oldState); - this.didTransition(this.currentRouteInfos!); - this.toInfos(newTransition, newState.routeInfos, true); - this.routeDidChange(newTransition); - } - return result; - }, - null, - promiseLabel('Transition complete') - ); - - return newTransition; - } - } - - transitionByIntent(intent: TransitionIntent, isIntermediate: boolean): InternalTransition { - try { - return this.getTransitionByIntent(intent, isIntermediate); - } catch (e) { - return new InternalTransition(this, intent, undefined, e, undefined); - } - } - - recognize(url: string): Option { - let intent = new URLTransitionIntent(this, url); - let newState = this.generateNewState(intent); - - if (newState === null) { - return newState; - } - - let readonlyInfos = toReadOnlyRouteInfo(newState.routeInfos, newState.queryParams, { - includeAttributes: false, - localizeMapUpdates: true, - }); - return readonlyInfos[readonlyInfos.length - 1] as RouteInfo; - } - - recognizeAndLoad(url: string): Promise { - let intent = new URLTransitionIntent(this, url); - let newState = this.generateNewState(intent); - - if (newState === null) { - return Promise.reject(`URL ${url} was not recognized`); - } - - let newTransition: OpaqueTransition = new InternalTransition(this, intent, newState, undefined); - return newTransition.then(() => { - let routeInfosWithAttributes = toReadOnlyRouteInfo( - newState!.routeInfos, - newTransition[QUERY_PARAMS_SYMBOL], - { - includeAttributes: true, - localizeMapUpdates: false, - } - ) as RouteInfoWithAttributes[]; - return routeInfosWithAttributes[routeInfosWithAttributes.length - 1]!; - }); - } - - private generateNewState(intent: TransitionIntent): Option> { - try { - return intent.applyToState(this.state!, false); - } catch (_e) { - return null; - } - } - - private getTransitionByIntent( - intent: TransitionIntent, - isIntermediate: boolean - ): InternalTransition { - let wasTransitioning = Boolean(this.activeTransition); - let oldState = wasTransitioning ? this.activeTransition![STATE_SYMBOL] : this.state; - let newTransition: InternalTransition; - - let newState = intent.applyToState(oldState!, isIntermediate); - let queryParamChangelist = getChangelist(oldState!.queryParams, newState.queryParams); - - if (routeInfosEqual(newState.routeInfos, oldState!.routeInfos)) { - // This is a no-op transition. See if query params changed. - if (queryParamChangelist) { - let newTransition = this.queryParamsTransition( - queryParamChangelist, - wasTransitioning, - oldState!, - newState - ); - newTransition.queryParamsOnly = true; - // SAFETY: The returned OpaqueTransition should actually be this. - return newTransition as InternalTransition; - } - - // No-op. No need to create a new transition. - return this.activeTransition || new InternalTransition(this, undefined, undefined); - } - - if (isIntermediate) { - let transition = new InternalTransition(this, undefined, newState); - transition.isIntermediate = true; - this.toReadOnlyInfos(transition, newState); - this.setupContexts(newState, transition); - - this.routeWillChange(transition); - return this.activeTransition!; - } - - // Create a new transition to the destination route. - newTransition = new InternalTransition( - this, - intent, - newState, - undefined, - this.activeTransition - ); - - // transition is to same route with same params, only query params differ. - // not caught above probably because refresh() has been used - if (routeInfosSameExceptQueryParams(newState.routeInfos, oldState!.routeInfos)) { - newTransition.queryParamsOnly = true; - } - - this.toReadOnlyInfos(newTransition, newState); - // Abort and usurp any previously active transition. - if (this.activeTransition) { - this.activeTransition.redirect(newTransition); - } - this.activeTransition = newTransition; - - // Transition promises by default resolve with resolved state. - // For our purposes, swap out the promise to resolve - // after the transition has been finalized. - newTransition.promise = newTransition.promise!.then( - (result: TransitionState) => { - return this.finalizeTransition(newTransition, result); - }, - null, - promiseLabel('Settle transition promise when transition is finalized') - ); - - if (!wasTransitioning) { - this.notifyExistingHandlers(newState, newTransition); - } - - this.fireQueryParamDidChange(newState, queryParamChangelist!); - - return newTransition; - } - - /** - @private - - Begins and returns a Transition based on the provided - arguments. Accepts arguments in the form of both URL - transitions and named transitions. - - @param {Router} router - @param {Array[Object]} args arguments passed to transitionTo, - replaceWith, or handleURL -*/ - private doTransition( - name?: string, - modelsArray: [...ModelFor[]] | [...ModelFor[], { queryParams: QueryParams }] = [], - isIntermediate = false - ): InternalTransition { - let lastArg = modelsArray[modelsArray.length - 1]; - let queryParams: Dict = {}; - - if (lastArg && Object.prototype.hasOwnProperty.call(lastArg, 'queryParams')) { - // We just checked this. - // TODO: Use an assertion? - queryParams = (modelsArray.pop() as { queryParams: QueryParams }) - .queryParams as Dict; - } - - let intent; - if (name === undefined) { - log(this, 'Updating query params'); - - // A query param update is really just a transition - // into the route you're already on. - let { routeInfos } = this.state!; - intent = new NamedTransitionIntent( - this, - routeInfos[routeInfos.length - 1]!.name, - undefined, - [], - queryParams - ); - } else if (name.charAt(0) === '/') { - log(this, 'Attempting URL transition to ' + name); - intent = new URLTransitionIntent(this, name); - } else { - log(this, 'Attempting transition to ' + name); - intent = new NamedTransitionIntent( - this, - name, - undefined, - // SAFETY: We know this to be the case since we removed the last item if it was QPs - modelsArray as ModelFor[], - queryParams - ); - } - - return this.transitionByIntent(intent, isIntermediate); - } - - /** - @private - - Updates the URL (if necessary) and calls `setupContexts` - to update the router's array of `currentRouteInfos`. - */ - private finalizeTransition( - transition: InternalTransition, - newState: TransitionState - ): R | Promise { - try { - log( - transition.router, - transition.sequence, - 'Resolved all models on destination route; finalizing transition.' - ); - - let routeInfos = newState.routeInfos; - - // Run all the necessary enter/setup/exit hooks - this.setupContexts(newState, transition); - - // Check if a redirect occurred in enter/setup - if (transition.isAborted) { - // TODO: cleaner way? distinguish b/w targetRouteInfos? - this.state!.routeInfos = this.currentRouteInfos!; - return Promise.reject(logAbort(transition)); - } - - this._updateURL(transition, newState); - - transition.isActive = false; - this.activeTransition = undefined; - - this.triggerEvent(this.currentRouteInfos!, true, 'didTransition', []); - this.didTransition(this.currentRouteInfos!); - this.toInfos(transition, newState.routeInfos, true); - this.routeDidChange(transition); - - log(this, transition.sequence, 'TRANSITION COMPLETE.'); - - // Resolve with the final route. - return routeInfos[routeInfos.length - 1]!.route!; - } catch (e) { - if (!isTransitionAborted(e)) { - let infos = transition[STATE_SYMBOL]!.routeInfos; - transition.trigger(true, 'error', e, transition, infos[infos.length - 1]!.route); - transition.abort(); - } - - throw e; - } - } - - /** - @private - - Takes an Array of `RouteInfo`s, figures out which ones are - exiting, entering, or changing contexts, and calls the - proper route hooks. - - For example, consider the following tree of routes. Each route is - followed by the URL segment it handles. - - ``` - |~index ("/") - | |~posts ("/posts") - | | |-showPost ("/:id") - | | |-newPost ("/new") - | | |-editPost ("/edit") - | |~about ("/about/:id") - ``` - - Consider the following transitions: - - 1. A URL transition to `/posts/1`. - 1. Triggers the `*model` callbacks on the - `index`, `posts`, and `showPost` routes - 2. Triggers the `enter` callback on the same - 3. Triggers the `setup` callback on the same - 2. A direct transition to `newPost` - 1. Triggers the `exit` callback on `showPost` - 2. Triggers the `enter` callback on `newPost` - 3. Triggers the `setup` callback on `newPost` - 3. A direct transition to `about` with a specified - context object - 1. Triggers the `exit` callback on `newPost` - and `posts` - 2. Triggers the `serialize` callback on `about` - 3. Triggers the `enter` callback on `about` - 4. Triggers the `setup` callback on `about` - - @param {Router} transition - @param {TransitionState} newState -*/ - private setupContexts(newState: TransitionState, transition?: InternalTransition) { - let partition = this.partitionRoutes(this.state!, newState); - let i, l, route; - - for (i = 0, l = partition.exited.length; i < l; i++) { - route = partition.exited[i]!.route; - delete route!.context; - - if (route !== undefined) { - if (route._internalReset !== undefined) { - route._internalReset(true, transition); - } - - if (route.exit !== undefined) { - route.exit(transition); - } - } - } - - let oldState = (this.oldState = this.state); - this.state = newState; - let currentRouteInfos = (this.currentRouteInfos = partition.unchanged.slice()); - - try { - for (i = 0, l = partition.reset.length; i < l; i++) { - route = partition.reset[i]!.route; - if (route !== undefined) { - if (route._internalReset !== undefined) { - route._internalReset(false, transition); - } - } - } - - for (i = 0, l = partition.updatedContext.length; i < l; i++) { - this.routeEnteredOrUpdated( - currentRouteInfos, - partition.updatedContext[i]!, - false, - transition! - ); - } - - for (i = 0, l = partition.entered.length; i < l; i++) { - this.routeEnteredOrUpdated(currentRouteInfos, partition.entered[i]!, true, transition!); - } - } catch (e) { - this.state = oldState; - this.currentRouteInfos = oldState!.routeInfos; - throw e; - } - - this.state.queryParams = this.finalizeQueryParamChange( - currentRouteInfos, - newState.queryParams, - transition! - ); - } - - /** - @private - - Fires queryParamsDidChange event -*/ - private fireQueryParamDidChange(newState: TransitionState, queryParamChangelist: ChangeList) { - // If queryParams changed trigger event - if (queryParamChangelist) { - // This is a little hacky but we need some way of storing - // changed query params given that no activeTransition - // is guaranteed to have occurred. - this._changedQueryParams = queryParamChangelist.all; - this.triggerEvent(newState.routeInfos, true, 'queryParamsDidChange', [ - queryParamChangelist.changed, - queryParamChangelist.all, - queryParamChangelist.removed, - ]); - this._changedQueryParams = undefined; - } - } - - /** - @private - - Helper method used by setupContexts. Handles errors or redirects - that may happen in enter/setup. -*/ - private routeEnteredOrUpdated( - currentRouteInfos: InternalRouteInfo[], - routeInfo: InternalRouteInfo, - enter: boolean, - transition?: InternalTransition - ) { - let route = routeInfo.route, - context = routeInfo.context; - - function _routeEnteredOrUpdated(route: R) { - if (enter) { - if (route.enter !== undefined) { - route.enter(transition!); - } - } - - throwIfAborted(transition); - - route.context = context as Awaited; - - if (route.contextDidChange !== undefined) { - route.contextDidChange(); - } - - if (route.setup !== undefined) { - route.setup(context!, transition!); - } - - throwIfAborted(transition); - - currentRouteInfos.push(routeInfo); - return route; - } - - // If the route doesn't exist, it means we haven't resolved the route promise yet - if (route === undefined) { - routeInfo.routePromise = routeInfo.routePromise.then(_routeEnteredOrUpdated); - } else { - _routeEnteredOrUpdated(route); - } - - return true; - } - - /** - @private - - This function is called when transitioning from one URL to - another to determine which routes are no longer active, - which routes are newly active, and which routes remain - active but have their context changed. - - Take a list of old routes and new routes and partition - them into four buckets: - - * unchanged: the route was active in both the old and - new URL, and its context remains the same - * updated context: the route was active in both the - old and new URL, but its context changed. The route's - `setup` method, if any, will be called with the new - context. - * exited: the route was active in the old URL, but is - no longer active. - * entered: the route was not active in the old URL, but - is now active. - - The PartitionedRoutes structure has four fields: - - * `updatedContext`: a list of `RouteInfo` objects that - represent routes that remain active but have a changed - context - * `entered`: a list of `RouteInfo` objects that represent - routes that are newly active - * `exited`: a list of `RouteInfo` objects that are no - longer active. - * `unchanged`: a list of `RouteInfo` objects that remain active. - - @param {Array[InternalRouteInfo]} oldRoutes a list of the route - information for the previous URL (or `[]` if this is the - first handled transition) - @param {Array[InternalRouteInfo]} newRoutes a list of the route - information for the new URL - - @return {Partition} -*/ - private partitionRoutes(oldState: TransitionState, newState: TransitionState) { - let oldRouteInfos = oldState.routeInfos; - let newRouteInfos = newState.routeInfos; - - let routes: RoutePartition = { - updatedContext: [], - exited: [], - entered: [], - unchanged: [], - reset: [], - }; - - let routeChanged, - contextChanged = false, - i, - l; - - for (i = 0, l = newRouteInfos.length; i < l; i++) { - let oldRouteInfo = oldRouteInfos[i]!, - newRouteInfo = newRouteInfos[i]!; - - if (!oldRouteInfo || oldRouteInfo.route !== newRouteInfo.route) { - routeChanged = true; - } - - if (routeChanged) { - routes.entered.push(newRouteInfo); - if (oldRouteInfo) { - routes.exited.unshift(oldRouteInfo); - } - } else if (contextChanged || oldRouteInfo.context !== newRouteInfo.context) { - contextChanged = true; - routes.updatedContext.push(newRouteInfo); - } else { - routes.unchanged.push(oldRouteInfo); - } - } - - for (i = newRouteInfos.length, l = oldRouteInfos.length; i < l; i++) { - routes.exited.unshift(oldRouteInfos[i]!); - } - - routes.reset = routes.updatedContext.slice(); - routes.reset.reverse(); - - return routes; - } - - private _updateURL(transition: OpaqueTransition, state: TransitionState) { - let urlMethod: string | null = transition.urlMethod; - - if (!urlMethod) { - return; - } - - let { routeInfos } = state; - let { name: routeName } = routeInfos[routeInfos.length - 1]!; - let params: Dict = {}; - - for (let i = routeInfos.length - 1; i >= 0; --i) { - let routeInfo = routeInfos[i]!; - merge(params, routeInfo.params); - if (routeInfo.route!.inaccessibleByURL) { - urlMethod = null; - } - } - - if (urlMethod) { - params['queryParams'] = transition._visibleQueryParams || state.queryParams; - let url = this.recognizer.generate(routeName, params as Params); - - // transitions during the initial transition must always use replaceURL. - // When the app boots, you are at a url, e.g. /foo. If some route - // redirects to bar as part of the initial transition, you don't want to - // add a history entry for /foo. If you do, pressing back will immediately - // hit the redirect again and take you back to /bar, thus killing the back - // button - let initial = transition.isCausedByInitialTransition; - - // say you are at / and you click a link to route /foo. In /foo's - // route, the transition is aborted using replaceWith('/bar'). - // Because the current url is still /, the history entry for / is - // removed from the history. Clicking back will take you to the page - // you were on before /, which is often not even the app, thus killing - // the back button. That's why updateURL is always correct for an - // aborting transition that's not the initial transition - let replaceAndNotAborting = - urlMethod === 'replace' && !transition.isCausedByAbortingTransition; - - // because calling refresh causes an aborted transition, this needs to be - // special cased - if the initial transition is a replace transition, the - // urlMethod should be honored here. - let isQueryParamsRefreshTransition = transition.queryParamsOnly && urlMethod === 'replace'; - - // say you are at / and you a `replaceWith(/foo)` is called. Then, that - // transition is aborted with `replaceWith(/bar)`. At the end, we should - // end up with /bar replacing /. We are replacing the replace. We only - // will replace the initial route if all subsequent aborts are also - // replaces. However, there is some ambiguity around the correct behavior - // here. - let replacingReplace = - urlMethod === 'replace' && transition.isCausedByAbortingReplaceTransition; - - if (initial || replaceAndNotAborting || isQueryParamsRefreshTransition || replacingReplace) { - this.replaceURL!(url); - } else { - this.updateURL(url); - } - } - } - - private finalizeQueryParamChange( - resolvedHandlers: InternalRouteInfo[], - newQueryParams: Dict, - transition: OpaqueTransition - ) { - // We fire a finalizeQueryParamChange event which - // gives the new route hierarchy a chance to tell - // us which query params it's consuming and what - // their final values are. If a query param is - // no longer consumed in the final route hierarchy, - // its serialized segment will be removed - // from the URL. - - for (let k in newQueryParams) { - if (newQueryParams.hasOwnProperty(k) && newQueryParams[k] === null) { - delete newQueryParams[k]; - } - } - - let finalQueryParamsArray: { - key: string; - value: string; - visible: boolean; - }[] = []; - - this.triggerEvent(resolvedHandlers, true, 'finalizeQueryParamChange', [ - newQueryParams, - finalQueryParamsArray, - transition, - ]); - - if (transition) { - transition._visibleQueryParams = {}; - } - - let finalQueryParams: Dict = {}; - for (let i = 0, len = finalQueryParamsArray.length; i < len; ++i) { - let qp = finalQueryParamsArray[i]!; - finalQueryParams[qp.key] = qp.value; - if (transition && qp.visible !== false) { - transition._visibleQueryParams[qp.key] = qp.value; - } - } - return finalQueryParams; - } - - private toReadOnlyInfos(newTransition: OpaqueTransition, newState: TransitionState) { - let oldRouteInfos = this.state!.routeInfos; - this.fromInfos(newTransition, oldRouteInfos); - this.toInfos(newTransition, newState.routeInfos); - this._lastQueryParams = newState.queryParams; - } - - private fromInfos(newTransition: OpaqueTransition, oldRouteInfos: InternalRouteInfo[]) { - if (newTransition !== undefined && oldRouteInfos.length > 0) { - let fromInfos = toReadOnlyRouteInfo(oldRouteInfos, Object.assign({}, this._lastQueryParams), { - includeAttributes: true, - localizeMapUpdates: false, - }) as RouteInfoWithAttributes[]; - newTransition!.from = fromInfos[fromInfos.length - 1] || null; - } - } - - public toInfos( - newTransition: OpaqueTransition, - newRouteInfos: InternalRouteInfo[], - includeAttributes = false - ) { - if (newTransition !== undefined && newRouteInfos.length > 0) { - let toInfos = toReadOnlyRouteInfo( - newRouteInfos, - Object.assign({}, newTransition[QUERY_PARAMS_SYMBOL]), - { includeAttributes, localizeMapUpdates: false } - ); - newTransition!.to = toInfos[toInfos.length - 1]! || null; - } - } - - private notifyExistingHandlers( - newState: TransitionState, - newTransition: InternalTransition - ) { - let oldRouteInfos = this.state!.routeInfos, - changing = [], - i, - oldRouteInfoLen, - oldHandler, - newRouteInfo; - - oldRouteInfoLen = oldRouteInfos.length; - for (i = 0; i < oldRouteInfoLen; i++) { - oldHandler = oldRouteInfos[i]!; - newRouteInfo = newState.routeInfos[i]; - - if (!newRouteInfo || oldHandler.name !== newRouteInfo.name) { - break; - } - - if (!newRouteInfo.isResolved) { - changing.push(oldHandler); - } - } - - this.triggerEvent(oldRouteInfos, true, 'willTransition', [newTransition]); - this.routeWillChange(newTransition); - this.willTransition(oldRouteInfos, newState.routeInfos, newTransition); - } - - /** - Clears the current and target route routes and triggers exit - on each of them starting at the leaf and traversing up through - its ancestors. - */ - reset() { - if (this.state) { - forEach>(this.state.routeInfos.slice().reverse(), function (routeInfo) { - let route = routeInfo.route; - if (route !== undefined) { - if (route.exit !== undefined) { - route.exit(); - } - } - return true; - }); - } - - this.oldState = undefined; - this.state = new TransitionState(); - this.currentRouteInfos = undefined; - } - - /** - let handler = routeInfo.handler; - The entry point for handling a change to the URL (usually - via the back and forward button). - - Returns an Array of handlers and the parameters associated - with those parameters. - - @param {String} url a URL to process - - @return {Array} an Array of `[handler, parameter]` tuples - */ - handleURL(url: string) { - // Perform a URL-based transition, but don't change - // the URL afterward, since it already happened. - if (url.charAt(0) !== '/') { - url = '/' + url; - } - - return this.doTransition(url)!.method(null); - } - - /** - Transition into the specified named route. - - If necessary, trigger the exit callback on any routes - that are no longer represented by the target route. - - @param {String} name the name of the route - */ - transitionTo(name: string | { queryParams: Dict }, ...contexts: any[]) { - if (typeof name === 'object') { - contexts.push(name); - return this.doTransition(undefined, contexts, false); - } - - return this.doTransition(name, contexts); - } - - intermediateTransitionTo(name: string, ...args: any[]) { - return this.doTransition(name, args, true); - } - - refresh(pivotRoute?: R) { - let previousTransition = this.activeTransition; - let state = previousTransition ? previousTransition[STATE_SYMBOL] : this.state; - let routeInfos = state!.routeInfos; - - if (pivotRoute === undefined) { - pivotRoute = routeInfos[0]!.route; - } - - log(this, 'Starting a refresh transition'); - let name = routeInfos[routeInfos.length - 1]!.name; - let intent = new NamedTransitionIntent( - this, - name, - pivotRoute, - [], - this._changedQueryParams || state!.queryParams - ); - - let newTransition = this.transitionByIntent(intent, false); - - // if the previous transition is a replace transition, that needs to be preserved - if (previousTransition && previousTransition.urlMethod === 'replace') { - newTransition.method(previousTransition.urlMethod); - } - - return newTransition; - } - - /** - Identical to `transitionTo` except that the current URL will be replaced - if possible. - - This method is intended primarily for use with `replaceState`. - - @param {String} name the name of the route - */ - replaceWith(name: string) { - return this.doTransition(name).method('replace'); - } - - /** - Take a named route and context objects and generate a - URL. - - @param {String} name the name of the route to generate - a URL for - @param {...Object} objects a list of objects to serialize - - @return {String} a URL - */ - generate(routeName: string, ...args: ModelsAndQueryParams>) { - let partitionedArgs = extractQueryParams(args), - suppliedParams = partitionedArgs[0], - queryParams = partitionedArgs[1]; - - // Construct a TransitionIntent with the provided params - // and apply it to the present state of the router. - let intent = new NamedTransitionIntent(this, routeName, undefined, suppliedParams); - let state = intent.applyToState(this.state!, false); - - let params: Params = {}; - for (let i = 0, len = state.routeInfos.length; i < len; ++i) { - let routeInfo = state.routeInfos[i]!; - let routeParams = routeInfo.serialize(); - merge(params, routeParams); - } - params.queryParams = queryParams; - - return this.recognizer.generate(routeName, params); - } - - applyIntent(routeName: string, contexts: ModelFor[]): TransitionState { - let intent = new NamedTransitionIntent(this, routeName, undefined, contexts); - - let state = (this.activeTransition && this.activeTransition[STATE_SYMBOL]) || this.state!; - - return intent.applyToState(state, false); - } - - isActiveIntent( - routeName: string, - contexts: ModelFor[], - queryParams?: Dict | null, - _state?: TransitionState - ) { - let state = _state || this.state!, - targetRouteInfos = state.routeInfos, - routeInfo, - len; - - if (!targetRouteInfos.length) { - return false; - } - - let targetHandler = targetRouteInfos[targetRouteInfos.length - 1]!.name; - let recognizerHandlers: ParsedHandler[] = this.recognizer.handlersFor(targetHandler); - - let index = 0; - for (len = recognizerHandlers.length; index < len; ++index) { - routeInfo = targetRouteInfos[index]!; - if (routeInfo.name === routeName) { - break; - } - } - - if (index === recognizerHandlers.length) { - // The provided route name isn't even in the route hierarchy. - return false; - } - - let testState = new TransitionState(); - testState.routeInfos = targetRouteInfos.slice(0, index + 1); - recognizerHandlers = recognizerHandlers.slice(0, index + 1); - - let intent = new NamedTransitionIntent(this, targetHandler, undefined, contexts); - - let newState = intent.applyToHandlers(testState, recognizerHandlers, targetHandler, true, true); - - let routesEqual = routeInfosEqual(newState.routeInfos, testState.routeInfos); - if (!queryParams || !routesEqual) { - return routesEqual; - } - - // Get a hash of QPs that will still be active on new route - let activeQPsOnNewHandler: Dict = {}; - merge(activeQPsOnNewHandler, queryParams); - - let activeQueryParams = state.queryParams; - for (let key in activeQueryParams) { - if (activeQueryParams.hasOwnProperty(key) && activeQPsOnNewHandler.hasOwnProperty(key)) { - activeQPsOnNewHandler[key] = activeQueryParams[key]; - } - } - - return routesEqual && !getChangelist(activeQPsOnNewHandler, queryParams); - } - - isActive(routeName: string, ...args: ModelsAndQueryParams>) { - let [contexts, queryParams] = extractQueryParams(args); - return this.isActiveIntent(routeName, contexts, queryParams); - } - - trigger(name: string, ...args: any[]) { - this.triggerEvent(this.currentRouteInfos!, false, name, args); - } -} - -function routeInfosEqual( - routeInfos: InternalRouteInfo[], - otherRouteInfos: InternalRouteInfo[] -) { - if (routeInfos.length !== otherRouteInfos.length) { - return false; - } - - for (let i = 0, len = routeInfos.length; i < len; ++i) { - // SAFETY: Just casting for comparison - if (routeInfos[i] !== (otherRouteInfos[i] as unknown as InternalRouteInfo)) { - return false; - } - } - return true; -} - -function routeInfosSameExceptQueryParams( - routeInfos: InternalRouteInfo[], - otherRouteInfos: InternalRouteInfo[] -) { - if (routeInfos.length !== otherRouteInfos.length) { - return false; - } - - for (let i = 0, len = routeInfos.length; i < len; ++i) { - if (routeInfos[i]!.name !== otherRouteInfos[i]!.name) { - return false; - } - - if (!paramsEqual(routeInfos[i]!.params, otherRouteInfos[i]!.params)) { - return false; - } - } - return true; -} - -function paramsEqual(params: Dict | undefined, otherParams: Dict | undefined) { - if (params === otherParams) { - // Both identical or both undefined - return true; - } - - if (!params || !otherParams) { - // One is falsy but other is not - return false; - } - - let keys = Object.keys(params); - let otherKeys = Object.keys(otherParams); - - if (keys.length !== otherKeys.length) { - return false; - } - - for (let i = 0, len = keys.length; i < len; ++i) { - let key = keys[i]!; - - if (params[key] !== otherParams[key]) { - return false; - } - } - - return true; -} - -export interface RoutePartition { - updatedContext: InternalRouteInfo[]; - exited: InternalRouteInfo[]; - entered: InternalRouteInfo[]; - unchanged: InternalRouteInfo[]; - reset: InternalRouteInfo[]; -} diff --git a/packages/router_js/lib/transition-aborted-error.ts b/packages/router_js/lib/transition-aborted-error.ts deleted file mode 100644 index 54b0049e15c..00000000000 --- a/packages/router_js/lib/transition-aborted-error.ts +++ /dev/null @@ -1,40 +0,0 @@ -export interface TransitionAbortedError extends Error { - name: 'TransitionAborted'; - code: 'TRANSITION_ABORTED'; -} - -export function buildTransitionAborted() { - let error = new Error('TransitionAborted') as TransitionAbortedError; - error.name = 'TransitionAborted'; - error.code = 'TRANSITION_ABORTED'; - return error; -} - -export function isTransitionAborted(maybeError: unknown): maybeError is TransitionAbortedError { - return ( - typeof maybeError === 'object' && - maybeError !== null && - (maybeError as TransitionAbortedError).code === 'TRANSITION_ABORTED' - ); -} - -interface Abortable { - isAborted: T; - [key: string]: unknown; -} - -function isAbortable(maybeAbortable: unknown): maybeAbortable is Abortable { - return ( - typeof maybeAbortable === 'object' && - maybeAbortable !== null && - typeof (maybeAbortable as Abortable).isAborted === 'boolean' - ); -} - -export function throwIfAborted(maybe: Abortable): never; -export function throwIfAborted(maybe: unknown): void; -export function throwIfAborted(maybe: unknown | Abortable): never | void { - if (isAbortable(maybe) && maybe.isAborted) { - throw buildTransitionAborted(); - } -} diff --git a/packages/router_js/lib/transition-intent.ts b/packages/router_js/lib/transition-intent.ts deleted file mode 100644 index d3c2595fdf2..00000000000 --- a/packages/router_js/lib/transition-intent.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Route } from './route-info'; -import type Router from './router'; -import type TransitionState from './transition-state'; - -export type OpaqueIntent = TransitionIntent; - -export abstract class TransitionIntent { - data: object; - router: Router; - constructor(router: Router, data: object = {}) { - this.router = router; - this.data = data; - } - preTransitionState?: TransitionState; - abstract applyToState(oldState: TransitionState, isIntermediate: boolean): TransitionState; -} diff --git a/packages/router_js/lib/transition-intent/named-transition-intent.ts b/packages/router_js/lib/transition-intent/named-transition-intent.ts deleted file mode 100644 index c99cf319797..00000000000 --- a/packages/router_js/lib/transition-intent/named-transition-intent.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -import type { Dict } from '../core'; -import type { ModelFor, ResolvedRouteInfo, Route } from '../route-info'; -import type InternalRouteInfo from '../route-info'; -import { UnresolvedRouteInfoByObject, UnresolvedRouteInfoByParam } from '../route-info'; -import type { ParsedHandler } from '../router'; -import type Router from '../router'; -import { TransitionIntent } from '../transition-intent'; -import TransitionState from '../transition-state'; -import { isParam, merge } from '../utils'; - -export default class NamedTransitionIntent extends TransitionIntent { - name: string; - pivotHandler?: Route; - contexts: ModelFor[]; - queryParams: Dict; - preTransitionState?: TransitionState = undefined; - - constructor( - router: Router, - name: string, - pivotHandler: Route | undefined, - contexts: ModelFor[] = [], - queryParams: Dict = {}, - data?: object - ) { - super(router, data); - this.name = name; - this.pivotHandler = pivotHandler; - this.contexts = contexts; - this.queryParams = queryParams; - } - - applyToState(oldState: TransitionState, isIntermediate: boolean): TransitionState { - let handlers: ParsedHandler[] = this.router.recognizer.handlersFor(this.name); - - let targetRouteName = handlers[handlers.length - 1]!.handler; - - return this.applyToHandlers(oldState, handlers, targetRouteName, isIntermediate, false); - } - - applyToHandlers( - oldState: TransitionState, - parsedHandlers: ParsedHandler[], - targetRouteName: string, - isIntermediate: boolean, - checkingIfActive: boolean - ) { - let i, len; - let newState = new TransitionState(); - let objects = this.contexts.slice(0); - - let invalidateIndex = parsedHandlers.length; - - // Pivot handlers are provided for refresh transitions - if (this.pivotHandler) { - for (i = 0, len = parsedHandlers.length; i < len; ++i) { - if (parsedHandlers[i]!.handler === this.pivotHandler._internalName) { - invalidateIndex = i; - break; - } - } - } - - for (i = parsedHandlers.length - 1; i >= 0; --i) { - let result = parsedHandlers[i]!; - let name = result.handler; - - let oldHandlerInfo = oldState.routeInfos[i]!; - let newHandlerInfo: - | InternalRouteInfo - | UnresolvedRouteInfoByObject - | ResolvedRouteInfo - | null = null; - - if (result.names.length > 0) { - if (i >= invalidateIndex) { - newHandlerInfo = this.createParamHandlerInfo(name, result.names, objects, oldHandlerInfo); - } else { - newHandlerInfo = this.getHandlerInfoForDynamicSegment( - name, - result.names, - objects, - oldHandlerInfo, - targetRouteName, - i - ); - } - } else { - // This route has no dynamic segment. - // Therefore treat as a param-based handlerInfo - // with empty params. This will cause the `model` - // hook to be called with empty params, which is desirable. - newHandlerInfo = this.createParamHandlerInfo(name, result.names, objects, oldHandlerInfo); - } - - if (checkingIfActive) { - // If we're performing an isActive check, we want to - // serialize URL params with the provided context, but - // ignore mismatches between old and new context. - newHandlerInfo = newHandlerInfo.becomeResolved( - null, - // SAFETY: This seems to imply that it would be resolved, but it's unclear if that's actually the case. - newHandlerInfo.context as Awaited - ); - let oldContext = oldHandlerInfo && oldHandlerInfo.context; - if ( - result.names.length > 0 && - oldHandlerInfo.context !== undefined && - newHandlerInfo.context === oldContext - ) { - // If contexts match in isActive test, assume params also match. - // This allows for flexibility in not requiring that every last - // handler provide a `serialize` method - newHandlerInfo.params = oldHandlerInfo && oldHandlerInfo.params; - } - newHandlerInfo.context = oldContext as Awaited; - } - - let handlerToUse: - | InternalRouteInfo - | UnresolvedRouteInfoByObject - | ResolvedRouteInfo = oldHandlerInfo; - - if (i >= invalidateIndex || newHandlerInfo.shouldSupersede(oldHandlerInfo)) { - invalidateIndex = Math.min(i, invalidateIndex); - handlerToUse = newHandlerInfo; - } - - if (isIntermediate && !checkingIfActive) { - handlerToUse = handlerToUse.becomeResolved( - null, - // SAFETY: This seems to imply that it would be resolved, but it's unclear if that's actually the case. - handlerToUse.context as ModelFor - ); - } - - newState.routeInfos.unshift(handlerToUse); - } - - if (objects.length > 0) { - throw new Error( - 'More context objects were passed than there are dynamic segments for the route: ' + - targetRouteName - ); - } - - if (!isIntermediate) { - this.invalidateChildren(newState.routeInfos, invalidateIndex); - } - - merge(newState.queryParams, this.queryParams || {}); - if (isIntermediate && oldState.queryParams) { - merge(newState.queryParams, oldState.queryParams); - } - - return newState; - } - - invalidateChildren(handlerInfos: InternalRouteInfo[], invalidateIndex: number) { - for (let i = invalidateIndex, l = handlerInfos.length; i < l; ++i) { - let handlerInfo = handlerInfos[i]!; - if (handlerInfo.isResolved) { - let { name, params, route, paramNames } = handlerInfos[i]!; - handlerInfos[i] = new UnresolvedRouteInfoByParam( - this.router, - name, - paramNames, - params, - route - ); - } - } - } - - getHandlerInfoForDynamicSegment( - name: string, - names: string[], - objects: ModelFor[], - oldHandlerInfo: InternalRouteInfo, - _targetRouteName: string, - i: number - ): UnresolvedRouteInfoByObject { - let objectToUse: ModelFor | PromiseLike> | undefined; - if (objects.length > 0) { - // Use the objects provided for this transition. - objectToUse = objects[objects.length - 1]; - if (isParam(objectToUse)) { - return this.createParamHandlerInfo(name, names, objects, oldHandlerInfo); - } else { - objects.pop(); - } - } else if (oldHandlerInfo && oldHandlerInfo.name === name) { - // Reuse the matching oldHandlerInfo - return oldHandlerInfo; - } else { - if (this.preTransitionState) { - let preTransitionHandlerInfo = this.preTransitionState.routeInfos[i] as - | ResolvedRouteInfo - | undefined; - objectToUse = preTransitionHandlerInfo?.context; - } else { - // Ideally we should throw this error to provide maximal - // information to the user that not enough context objects - // were provided, but this proves too cumbersome in Ember - // in cases where inner template helpers are evaluated - // before parent helpers un-render, in which cases this - // error somewhat prematurely fires. - //throw new Error("Not enough context objects were provided to complete a transition to " + targetRouteName + ". Specifically, the " + name + " route needs an object that can be serialized into its dynamic URL segments [" + names.join(', ') + "]"); - return oldHandlerInfo; - } - } - - return new UnresolvedRouteInfoByObject(this.router, name, names, objectToUse); - } - - createParamHandlerInfo( - name: string, - names: string[], - objects: unknown[], - oldHandlerInfo: InternalRouteInfo - ) { - let params: Dict = {}; - - // Soak up all the provided string/numbers - let numNames = names.length; - let missingParams = []; - while (numNames--) { - // Only use old params if the names match with the new handler - let oldParams = - (oldHandlerInfo && name === oldHandlerInfo.name && oldHandlerInfo.params) || {}; - - let peek = objects[objects.length - 1]; - let paramName = names[numNames]!; - if (isParam(peek)) { - params[paramName] = String(objects.pop()); - } else { - // If we're here, this means only some of the params - // were string/number params, so try and use a param - // value from a previous handler. - if (oldParams.hasOwnProperty(paramName)) { - params[paramName] = oldParams[paramName]; - } else { - missingParams.push(paramName); - } - } - } - if (missingParams.length > 0) { - throw new Error( - `You didn't provide enough string/numeric parameters to satisfy all of the dynamic segments for route ${name}.` + - ` Missing params: ${missingParams}` - ); - } - - return new UnresolvedRouteInfoByParam(this.router, name, names, params); - } -} diff --git a/packages/router_js/lib/transition-intent/url-transition-intent.ts b/packages/router_js/lib/transition-intent/url-transition-intent.ts deleted file mode 100644 index ff5cfde36f7..00000000000 --- a/packages/router_js/lib/transition-intent/url-transition-intent.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { Route } from '../route-info'; -import { UnresolvedRouteInfoByParam } from '../route-info'; -import type Router from '../router'; -import { TransitionIntent } from '../transition-intent'; -import TransitionState from '../transition-state'; -import UnrecognizedURLError from '../unrecognized-url-error'; -import { merge } from '../utils'; - -export default class URLTransitionIntent extends TransitionIntent { - preTransitionState?: TransitionState; - url: string; - constructor(router: Router, url: string, data?: object) { - super(router, data); - this.url = url; - this.preTransitionState = undefined; - } - - applyToState(oldState: TransitionState) { - let newState = new TransitionState(); - let results = this.router.recognizer.recognize(this.url), - i, - len; - - if (!results) { - throw new UnrecognizedURLError(this.url); - } - - let statesDiffer = false; - let _url = this.url; - - // Checks if a handler is accessible by URL. If it is not, an error is thrown. - // For the case where the handler is loaded asynchronously, the error will be - // thrown once it is loaded. - function checkHandlerAccessibility(handler: R) { - if (handler && handler.inaccessibleByURL) { - throw new UnrecognizedURLError(_url); - } - - return handler; - } - - for (i = 0, len = results.length; i < len; ++i) { - let result = results[i]!; - let name = result.handler as string; - let paramNames: string[] = []; - - if (this.router.recognizer.hasRoute(name)) { - paramNames = this.router.recognizer.handlersFor(name)[i].names; - } - - let newRouteInfo = new UnresolvedRouteInfoByParam( - this.router, - name, - paramNames, - result.params - ); - - let route = newRouteInfo.route; - - if (route) { - checkHandlerAccessibility(route); - } else { - // If the handler is being loaded asynchronously, check if we can - // access it after it has resolved - newRouteInfo.routePromise = newRouteInfo.routePromise.then(checkHandlerAccessibility); - } - - let oldRouteInfo = oldState.routeInfos[i]!; - if (statesDiffer || newRouteInfo.shouldSupersede(oldRouteInfo)) { - statesDiffer = true; - newState.routeInfos[i] = newRouteInfo; - } else { - newState.routeInfos[i] = oldRouteInfo; - } - } - - merge(newState.queryParams, results.queryParams); - - return newState; - } -} diff --git a/packages/router_js/lib/transition-state.ts b/packages/router_js/lib/transition-state.ts deleted file mode 100644 index e10c7c7361c..00000000000 --- a/packages/router_js/lib/transition-state.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Promise } from 'rsvp'; -import type { Dict } from './core'; -import type { Route, ResolvedRouteInfo } from './route-info'; -import type InternalRouteInfo from './route-info'; -import type Transition from './transition'; -import { forEach, promiseLabel } from './utils'; -import { throwIfAborted } from './transition-aborted-error'; - -interface IParams { - [key: string]: unknown; -} - -function handleError( - currentState: TransitionState, - transition: Transition, - error: Error -): never { - // This is the only possible - // reject value of TransitionState#resolve - let routeInfos = currentState.routeInfos; - let errorHandlerIndex = - transition.resolveIndex >= routeInfos.length ? routeInfos.length - 1 : transition.resolveIndex; - - let wasAborted = transition.isAborted; - - throw new TransitionError( - error, - currentState.routeInfos[errorHandlerIndex]!.route!, - wasAborted, - currentState - ); -} - -function resolveOneRouteInfo( - currentState: TransitionState, - transition: Transition -): void | Promise { - if (transition.resolveIndex === currentState.routeInfos.length) { - // This is is the only possible - // fulfill value of TransitionState#resolve - return; - } - - let routeInfo = currentState.routeInfos[transition.resolveIndex]!; - - let callback = proceed.bind(null, currentState, transition) as ( - resolvedRouteInfo: ResolvedRouteInfo - ) => void | Promise; - - return routeInfo.resolve(transition).then(callback, null, currentState.promiseLabel('Proceed')); -} - -function proceed( - currentState: TransitionState, - transition: Transition, - resolvedRouteInfo: ResolvedRouteInfo -): void | Promise { - let wasAlreadyResolved = currentState.routeInfos[transition.resolveIndex]!.isResolved; - - // Swap the previously unresolved routeInfo with - // the resolved routeInfo - currentState.routeInfos[transition.resolveIndex++] = resolvedRouteInfo; - - if (!wasAlreadyResolved) { - // Call the redirect hook. The reason we call it here - // vs. afterModel is so that redirects into child - // routes don't re-run the model hooks for this - // already-resolved route. - let { route } = resolvedRouteInfo; - if (route !== undefined) { - if (route.redirect) { - route.redirect(resolvedRouteInfo.context, transition); - } - } - } - - // Proceed after ensuring that the redirect hook - // didn't abort this transition by transitioning elsewhere. - throwIfAborted(transition); - - return resolveOneRouteInfo(currentState, transition); -} - -export default class TransitionState { - routeInfos: InternalRouteInfo[] = []; - queryParams: Dict = {}; - params: IParams = {}; - - promiseLabel(label: string) { - let targetName = ''; - forEach(this.routeInfos, function (routeInfo) { - if (targetName !== '') { - targetName += '.'; - } - targetName += routeInfo.name; - return true; - }); - return promiseLabel("'" + targetName + "': " + label); - } - - resolve(transition: Transition): Promise> { - // First, calculate params for this state. This is useful - // information to provide to the various route hooks. - let params = this.params; - forEach(this.routeInfos, (routeInfo) => { - params[routeInfo.name] = routeInfo.params || {}; - return true; - }); - - transition.resolveIndex = 0; - - let callback = resolveOneRouteInfo.bind(null, this, transition); - let errorHandler = handleError.bind(null, this, transition); - - // The prelude RSVP.resolve() async moves us into the promise land. - return Promise.resolve(null, this.promiseLabel('Start transition')) - .then(callback, null, this.promiseLabel('Resolve route')) - .catch(errorHandler, this.promiseLabel('Handle error')) - .then(() => this); - } -} - -export class TransitionError { - constructor( - public error: Error, - public route: Route, - public wasAborted: boolean, - public state: TransitionState - ) {} -} diff --git a/packages/router_js/lib/transition.ts b/packages/router_js/lib/transition.ts deleted file mode 100644 index b4c0e26634c..00000000000 --- a/packages/router_js/lib/transition.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { Promise } from 'rsvp'; -import type { Dict, Maybe, Option } from './core'; -import type { ModelFor, Route, RouteInfo, RouteInfoWithAttributes } from './route-info'; -import type InternalRouteInfo from './route-info'; -import type Router from './router'; -import type { TransitionAbortedError } from './transition-aborted-error'; -import { buildTransitionAborted } from './transition-aborted-error'; -import type { OpaqueIntent } from './transition-intent'; -import type { TransitionError } from './transition-state'; -import type TransitionState from './transition-state'; -import { log, promiseLabel } from './utils'; -import { DEBUG } from '@glimmer/env'; - -export type OnFulfilled = - | ((value: T) => TResult1 | PromiseLike) - | undefined - | null; -export type OnRejected = - | ((reason: T) => TResult2 | PromiseLike) - | undefined - | null; - -export type PublicTransition = Transition; -export type OpaqueTransition = PublicTransition; - -export const STATE_SYMBOL = `__STATE__-2619860001345920-3322w3`; -export const PARAMS_SYMBOL = `__PARAMS__-261986232992830203-23323`; -export const QUERY_PARAMS_SYMBOL = `__QPS__-2619863929824844-32323`; -export const REDIRECT_DESTINATION_SYMBOL = `__RDS__-2619863929824844-32323`; - -/** - A Transition is a thenable (a promise-like object) that represents - an attempt to transition to another route. It can be aborted, either - explicitly via `abort` or by attempting another transition while a - previous one is still underway. An aborted transition can also - be `retry()`d later. - - @class Transition - @constructor - @param {Object} router - @param {Object} intent - @param {Object} state - @param {Object} error - @private - */ -export default class Transition implements Partial> { - [STATE_SYMBOL]: TransitionState; - from: Maybe = null; - to?: RouteInfo | RouteInfoWithAttributes = undefined; - router: Router; - data: Dict; - intent: Maybe; - resolvedModels: Dict | undefined>; - [QUERY_PARAMS_SYMBOL]: Dict; - promise?: Promise; // Todo: Fix this shit its actually TransitionState | IHandler | undefined | Error - error: Maybe; - [PARAMS_SYMBOL]: Dict; - routeInfos: InternalRouteInfo[]; - targetName: Maybe; - pivotHandler: Maybe; - sequence: number; - isAborted = false; - isActive = true; - urlMethod: Option = 'update'; - resolveIndex = 0; - queryParamsOnly = false; - isTransition = true; - isCausedByAbortingTransition = false; - isCausedByInitialTransition = false; - isCausedByAbortingReplaceTransition = false; - _visibleQueryParams: Dict = {}; - isIntermediate = false; - [REDIRECT_DESTINATION_SYMBOL]?: Transition; - - /** - In non-production builds, this function will return the stack that this Transition was - created within. In production builds, this function will not be present. - - @method debugCreationStack - @return string - */ - declare debugCreationStack?: () => string | undefined; - - /** - In non-production builds, this function will return the stack that this Transition was - aborted within (or `undefined` if the Transition has not been aborted yet). In production - builds, this function will not be present. - - @method debugAbortStack - @return string - */ - declare debugAbortStack?: () => string | undefined; - - /** - In non-production builds, this property references the Transition that _this_ Transition - was derived from or `undefined` if this transition did not derive from another. In - production builds, this property will not be present. - - @property debugPreviousTransition - @type {Transition | undefined} - */ - declare debugPreviousTransition: Maybe>; - - constructor( - router: Router, - intent: Maybe, - state: TransitionState | undefined, - error: Maybe = undefined, - previousTransition: Maybe> = undefined - ) { - this[STATE_SYMBOL] = state! || router.state!; - this.intent = intent; - this.router = router; - this.data = (intent && intent.data) || {}; - this.resolvedModels = {}; - this[QUERY_PARAMS_SYMBOL] = {}; - this.promise = undefined; - this.error = undefined; - this[PARAMS_SYMBOL] = {}; - this.routeInfos = []; - this.targetName = undefined; - this.pivotHandler = undefined; - this.sequence = -1; - - if (DEBUG) { - let error = new Error(`Transition creation stack`); - - this.debugCreationStack = () => error.stack; - - // not aborted yet, will be replaced when `this.isAborted` is set - this.debugAbortStack = () => undefined; - - this.debugPreviousTransition = previousTransition; - } - - if (error) { - this.promise = Promise.reject(error); - this.error = error; - return; - } - - // if you're doing multiple redirects, need the new transition to know if it - // is actually part of the first transition or not. Any further redirects - // in the initial transition also need to know if they are part of the - // initial transition - this.isCausedByAbortingTransition = Boolean(previousTransition); - this.isCausedByInitialTransition = - Boolean(previousTransition) && - (previousTransition!.isCausedByInitialTransition || previousTransition!.sequence === 0); - // Every transition in the chain is a replace - this.isCausedByAbortingReplaceTransition = - Boolean(previousTransition) && - previousTransition!.urlMethod === 'replace' && - (!previousTransition!.isCausedByAbortingTransition || - previousTransition!.isCausedByAbortingReplaceTransition); - - if (state) { - this[PARAMS_SYMBOL] = state.params; - this[QUERY_PARAMS_SYMBOL] = state.queryParams; - this.routeInfos = state.routeInfos; - - let len = state.routeInfos.length; - if (len) { - this.targetName = state.routeInfos[len - 1]!.name; - } - - for (let i = 0; i < len; ++i) { - let handlerInfo = state.routeInfos[i]!; - - // TODO: this all seems hacky - if (!handlerInfo.isResolved) { - break; - } - this.pivotHandler = handlerInfo.route; - } - - this.sequence = router.currentSequence++; - this.promise = state.resolve(this).catch((result: TransitionError) => { - let error = this.router.transitionDidError(result, this); - - throw error; - }, promiseLabel('Handle Abort')); - } else { - this.promise = Promise.resolve(this[STATE_SYMBOL]!); - this[PARAMS_SYMBOL] = {}; - } - } - - /** - The Transition's internal promise. Calling `.then` on this property - is that same as calling `.then` on the Transition object itself, but - this property is exposed for when you want to pass around a - Transition's promise, but not the Transition object itself, since - Transition object can be externally `abort`ed, while the promise - cannot. - - @property promise - @type {Object} - @public - */ - - /** - Custom state can be stored on a Transition's `data` object. - This can be useful for decorating a Transition within an earlier - hook and shared with a later hook. Properties set on `data` will - be copied to new transitions generated by calling `retry` on this - transition. - - @property data - @type {Object} - @public - */ - - /** - A standard promise hook that resolves if the transition - succeeds and rejects if it fails/redirects/aborts. - - Forwards to the internal `promise` property which you can - use in situations where you want to pass around a thenable, - but not the Transition itself. - - @method then - @param {Function} onFulfilled - @param {Function} onRejected - @param {String} label optional string for labeling the promise. - Useful for tooling. - @return {Promise} - @public - */ - then( - onFulfilled?: ((value: R) => TResult1 | PromiseLike) | undefined | null, - onRejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null, - label?: string - ): Promise { - return this.promise!.then(onFulfilled, onRejected, label); - } - - /** - - Forwards to the internal `promise` property which you can - use in situations where you want to pass around a thennable, - but not the Transition itself. - - @method catch - @param {Function} onRejection - @param {String} label optional string for labeling the promise. - Useful for tooling. - @return {Promise} - @public - */ - catch(onRejection?: OnRejected, T>, label?: string) { - return this.promise!.catch(onRejection, label); - } - - /** - - Forwards to the internal `promise` property which you can - use in situations where you want to pass around a thenable, - but not the Transition itself. - - @method finally - @param {Function} callback - @param {String} label optional string for labeling the promise. - Useful for tooling. - @return {Promise} - @public - */ - finally(callback?: T | undefined, label?: string) { - // @ts-expect-error @types/rsvp doesn't have the correct signiture for RSVP.Promise.finally - return this.promise!.finally(callback, label); - } - - /** - Aborts the Transition. Note you can also implicitly abort a transition - by initiating another transition while a previous one is underway. - - @method abort - @return {Transition} this transition - @public - */ - abort() { - this.rollback(); - let transition = new Transition(this.router, undefined, undefined, undefined); - transition.to = this.from as RouteInfoWithAttributes; - transition.from = this.from; - transition.isAborted = true; - this.router.routeWillChange(transition); - this.router.routeDidChange(transition); - return this; - } - - rollback() { - if (!this.isAborted) { - log(this.router, this.sequence, this.targetName + ': transition was aborted'); - - if (DEBUG) { - let error = new Error(`Transition aborted stack`); - - this.debugAbortStack = () => error.stack; - } - - if (this.intent !== undefined && this.intent !== null) { - this.intent.preTransitionState = this.router.state; - } - - this.isAborted = true; - this.isActive = false; - this.router.activeTransition = undefined; - } - } - - redirect(newTransition: Transition) { - this[REDIRECT_DESTINATION_SYMBOL] = newTransition; - this.rollback(); - this.router.routeWillChange(newTransition); - } - - /** - - Retries a previously-aborted transition (making sure to abort the - transition if it's still active). Returns a new transition that - represents the new attempt to transition. - - @method retry - @return {Transition} new transition - @public - */ - retry() { - // TODO: add tests for merged state retry()s - this.abort(); - let newTransition = this.router.transitionByIntent(this.intent as OpaqueIntent, false); - - // inheriting a `null` urlMethod is not valid - // the urlMethod is only set to `null` when - // the transition is initiated *after* the url - // has been updated (i.e. `router.handleURL`) - // - // in that scenario, the url method cannot be - // inherited for a new transition because then - // the url would not update even though it should - if (this.urlMethod !== null) { - newTransition.method(this.urlMethod); - } - return newTransition; - } - - /** - - Sets the URL-changing method to be employed at the end of a - successful transition. By default, a new Transition will just - use `updateURL`, but passing 'replace' to this method will - cause the URL to update using 'replaceWith' instead. Omitting - a parameter will disable the URL change, allowing for transitions - that don't update the URL at completion (this is also used for - handleURL, since the URL has already changed before the - transition took place). - - @method method - @param {String} method the type of URL-changing method to use - at the end of a transition. Accepted values are 'replace', - falsy values, or any other non-falsy value (which is - interpreted as an updateURL transition). - - @return {Transition} this transition - @public - */ - method(method: Option) { - this.urlMethod = method; - return this; - } - - // Alias 'trigger' as 'send' - send( - ignoreFailure = false, - _name: string, - err?: Error, - transition?: Transition, - handler?: Route - ) { - this.trigger(ignoreFailure, _name, err, transition, handler); - } - - /** - - Fires an event on the current list of resolved/resolving - handlers within this transition. Useful for firing events - on route hierarchies that haven't fully been entered yet. - - Note: This method is also aliased as `send` - - @method trigger - @param {Boolean} [ignoreFailure=false] a boolean specifying whether unhandled events throw an error - @param {String} name the name of the event to fire - @public - */ - trigger(ignoreFailure = false, name: string, ...args: any[]) { - // TODO: Deprecate the current signature - if (typeof ignoreFailure === 'string') { - name = ignoreFailure; - ignoreFailure = false; - } - - this.router.triggerEvent( - this[STATE_SYMBOL]!.routeInfos.slice(0, this.resolveIndex + 1), - ignoreFailure, - name, - args - ); - } - - /** - Transitions are aborted and their promises rejected - when redirects occur; this method returns a promise - that will follow any redirects that occur and fulfill - with the value fulfilled by any redirecting transitions - that occur. - - @method followRedirects - @return {Promise} a promise that fulfills with the same - value that the final redirecting transition fulfills with - @public - */ - followRedirects(): Promise { - return this.promise!.catch((reason) => { - if (this[REDIRECT_DESTINATION_SYMBOL]) { - return this[REDIRECT_DESTINATION_SYMBOL]!.followRedirects(); - } - return Promise.reject(reason); - }); - } - - toString() { - return 'Transition (sequence ' + this.sequence + ')'; - } - - /** - @private - */ - log(message: string) { - log(this.router, this.sequence, message); - } -} - -/** - @private - - Logs and returns an instance of TransitionAborted. - */ -export function logAbort(transition: Transition): TransitionAbortedError { - log(transition.router, transition.sequence, 'detected abort.'); - - return buildTransitionAborted(); -} - -export function isTransition(obj: unknown): obj is typeof Transition { - return typeof obj === 'object' && obj instanceof Transition && obj.isTransition; -} - -export function prepareResult(obj: Dict | undefined) { - if (isTransition(obj)) { - return null; - } - - return obj; -} diff --git a/packages/router_js/lib/unrecognized-url-error.ts b/packages/router_js/lib/unrecognized-url-error.ts deleted file mode 100644 index b64b6514fd5..00000000000 --- a/packages/router_js/lib/unrecognized-url-error.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface UnrecognizedURLConstructor { - new (message?: string): UnrecognizedURLError; - readonly prototype: UnrecognizedURLError; -} - -export interface UnrecognizedURLError extends Error { - constructor: UnrecognizedURLConstructor; -} - -const UnrecognizedURLError: UnrecognizedURLConstructor = (function () { - UnrecognizedURLError.prototype = Object.create(Error.prototype); - UnrecognizedURLError.prototype.constructor = UnrecognizedURLError; - - function UnrecognizedURLError(this: UnrecognizedURLError, message?: string) { - let error = Error.call(this, message); - this.name = 'UnrecognizedURLError'; - this.message = message || 'UnrecognizedURL'; - - // @ts-expect-error I don't know why this is failing - if (Error.captureStackTrace) { - // @ts-expect-error I don't know why this is failing - Error.captureStackTrace(this, UnrecognizedURLError); - } else { - this.stack = error.stack; - } - } - - return UnrecognizedURLError as any; -})(); - -export default UnrecognizedURLError; diff --git a/packages/router_js/lib/utils.ts b/packages/router_js/lib/utils.ts deleted file mode 100644 index 38912f6c240..00000000000 --- a/packages/router_js/lib/utils.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { QueryParams } from 'route-recognizer'; -import type { Promise } from 'rsvp'; -import type { Dict } from './core'; -import type Router from './router'; - -export const slice = Array.prototype.slice; -const hasOwnProperty = Object.prototype.hasOwnProperty; - -/** - Determines if an object is Promise by checking if it is "thenable". -**/ -export function isPromise(p: any): p is Promise { - return p !== null && typeof p === 'object' && typeof p.then === 'function'; -} - -export function merge(hash: Dict, other?: Dict) { - for (let prop in other) { - if (hasOwnProperty.call(other, prop)) { - hash[prop] = other[prop]; - } - } -} - -export type ModelsAndQueryParams = T[] | [...T[], QueryParamsContainer]; - -/** - @private - - Extracts query params from the end of an array -**/ -export function extractQueryParams(array: ModelsAndQueryParams): [T[], QueryParams | null] { - let len = array && array.length, - head, - queryParams; - - if (len && len > 0) { - let obj = array[len - 1]; - if (isQueryParamsContainer(obj)) { - queryParams = obj.queryParams; - head = slice.call(array, 0, len - 1); - return [head, queryParams]; - } - } - - // SAFETY: We confirmed that the last item isn't a QP container - return [array as T[], null]; -} - -export type QueryParamsContainer = { queryParams: QueryParams }; - -// TODO: Actually check that Dict is QueryParams -function isQueryParamsContainer(obj: unknown): obj is QueryParamsContainer { - if (obj && typeof obj === 'object') { - let cast = obj as QueryParamsContainer; - return ( - 'queryParams' in cast && Object.keys(cast.queryParams).every((k) => typeof k === 'string') - ); - } - return false; -} - -/** - @private - - Coerces query param properties and array elements into strings. -**/ -export function coerceQueryParamsToString(queryParams: Dict) { - for (let key in queryParams) { - let val = queryParams[key]; - if (typeof val === 'number') { - queryParams[key] = String(val); - } else if (Array.isArray(val)) { - for (let i = 0, l = val.length; i < l; i++) { - val[i] = String(val[i]); - } - } - } -} -/** - @private - */ -export function log(router: Router, ...args: (string | number)[]): void { - if (!router.log) { - return; - } - - if (args.length === 2) { - let [sequence, msg] = args; - router.log('Transition #' + sequence + ': ' + msg); - } else { - let [msg] = args; - router.log(msg as string); - } -} - -export function isParam(object: any): object is string | number { - return ( - typeof object === 'string' || - object instanceof String || - typeof object === 'number' || - object instanceof Number - ); -} - -export function forEach(array: T[], callback: (item: T) => boolean) { - for (let i = 0, l = array.length; i < l && callback(array[i]!) !== false; i++) { - // empty intentionally - } -} - -export interface ChangeList { - all: Dict; - changed: Dict; - removed: Dict; -} - -export function getChangelist( - oldObject: Dict, - newObject: Dict -): ChangeList | undefined { - let key; - let results: ChangeList = { - all: {}, - changed: {}, - removed: {}, - }; - - merge(results.all, newObject); - - let didChange = false; - coerceQueryParamsToString(oldObject); - coerceQueryParamsToString(newObject); - - // Calculate removals - for (key in oldObject) { - if (hasOwnProperty.call(oldObject, key)) { - if (!hasOwnProperty.call(newObject, key)) { - didChange = true; - results.removed[key] = oldObject[key]; - } - } - } - - // Calculate changes - for (key in newObject) { - if (hasOwnProperty.call(newObject, key)) { - let oldElement = oldObject[key]; - let newElement = newObject[key]; - if (isArray(oldElement) && isArray(newElement)) { - if (oldElement.length !== newElement.length) { - results.changed[key] = newObject[key]; - didChange = true; - } else { - for (let i = 0, l = oldElement.length; i < l; i++) { - if (oldElement[i] !== newElement[i]) { - results.changed[key] = newObject[key]; - didChange = true; - } - } - } - } else if (oldObject[key] !== newObject[key]) { - results.changed[key] = newObject[key]; - didChange = true; - } - } - } - - return didChange ? results : undefined; -} - -function isArray(obj: unknown): obj is ArrayLike { - return Array.isArray(obj); -} - -export function promiseLabel(label: string) { - return 'Router: ' + label; -} diff --git a/packages/router_js/package.json b/packages/router_js/package.json deleted file mode 100644 index 68628f0ac24..00000000000 --- a/packages/router_js/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "router_js", - "version": "8.0.6", - "description": "A lightweight JavaScript library is built on top of route-recognizer and rsvp.js to provide an API for handling routes", - "license": "MIT", - "author": "Tilde, Inc.", - "private": true, - "exports": { - ".": "./index.ts" - }, - "scripts": { - }, - "dependencies": { - "@glimmer/env": "workspace:*" - }, - "devDependencies": { - "@types/qunit": "^2.9.6", - "@types/rsvp": "^4.0.4", - "backburner.js": "^2.6.0", - "loader.js": "^4.7.0", - "qunit": "^2.11.3", - "route-recognizer": "^0.3.4", - "rsvp": "^4.8.5" - }, - "peerDependencies": { - "route-recognizer": "^0.3.4", - "rsvp": "^4.8.5" - }, - "publishConfig": { - "registry": "https://registry.npmjs.org" - }, - "namespace": "Router" -} diff --git a/packages/router_js/tests/async_get_handler_test.ts b/packages/router_js/tests/async_get_handler_test.ts deleted file mode 100644 index 92835f5e6f9..00000000000 --- a/packages/router_js/tests/async_get_handler_test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { Route } from '../index'; -import type { Dict } from '../lib/core'; -import { Promise } from 'rsvp'; -import { createHandler, TestRouter } from './test_helpers'; - -function map(router: TestRouter) { - router.map(function (match) { - match('/index').to('index'); - match('/foo').to('foo', function (match) { - match('/').to('fooIndex'); - match('/bar').to('fooBar'); - }); - }); -} - -// 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 -let routes: Dict; -let router: TestRouter; -QUnit.module('Async Get Handler', { - beforeEach: function () { - QUnit.config.testTimeout = 60000; - routes = {}; - }, - - afterEach: function () { - QUnit.config.testTimeout = 1000; - }, -}); - -QUnit.test('can transition to lazily-resolved routes', function (assert) { - let done = assert.async(); - - class LazyRouter extends TestRouter { - getRoute(name: string) { - return new Promise(function (resolve) { - setTimeout(function () { - resolve(routes[name] || (routes[name] = createHandler('empty'))); - }, 1); - }); - } - } - - router = new LazyRouter(); - map(router); - - let fooCalled = false; - let fooBarCalled = false; - - routes['foo'] = createHandler('foo', { - model() { - fooCalled = true; - }, - }); - routes['fooBar'] = createHandler('fooBar', { - model: function () { - fooBarCalled = true; - }, - }); - - router.transitionTo('/foo/bar').then(function () { - assert.ok(fooCalled, 'foo is called before transition ends'); - assert.ok(fooBarCalled, 'fooBar is called before transition ends'); - done(); - }); - - assert.notOk(fooCalled, 'foo is not called synchronously'); - assert.notOk(fooBarCalled, 'fooBar is not called synchronously'); -}); - -QUnit.test('calls hooks of lazily-resolved routes in order', function (assert) { - let done = assert.async(); - let operations: string[] = []; - - class LazyRouter extends TestRouter { - getRoute(name: string) { - operations.push('get handler ' + name); - return new Promise(function (resolve) { - let timeoutLength = name === 'foo' ? 100 : 1; - setTimeout(function () { - operations.push('resolved ' + name); - resolve(routes[name] || (routes[name] = createHandler('empty'))); - }, timeoutLength); - }); - } - } - - router = new LazyRouter(); - map(router); - - routes['foo'] = createHandler('foo', { - model: function () { - operations.push('model foo'); - }, - }); - routes['fooBar'] = createHandler('fooBar', { - model: function () { - operations.push('model fooBar'); - }, - }); - - router.transitionTo('/foo/bar').then(function () { - assert.deepEqual( - operations, - [ - 'get handler foo', - 'get handler fooBar', - 'resolved fooBar', - 'resolved foo', - 'model foo', - 'model fooBar', - ], - 'order of operations is correct' - ); - done(); - }, null); -}); diff --git a/packages/router_js/tests/query_params_test.ts b/packages/router_js/tests/query_params_test.ts deleted file mode 100644 index d816289fa9a..00000000000 --- a/packages/router_js/tests/query_params_test.ts +++ /dev/null @@ -1,637 +0,0 @@ -/* eslint-disable qunit/no-conditional-assertions */ -import type { MatchCallback } from 'route-recognizer'; -import type { Route, Transition } from '../index'; -import type Router from '../index'; -import type { Dict, Maybe } from '../lib/core'; -import type RouteInfo from '../lib/route-info'; -import { Promise } from 'rsvp'; -import { createHandler, TestRouter, trigger, ignoreTransitionError } from './test_helpers'; - -let router: Router, handlers: Dict, expectedUrl: Maybe; -let scenarios = [ - { - name: 'Sync Get Handler', - getHandler: function (name: string) { - return handlers[name] || (handlers[name] = createHandler('empty')); - }, - }, - { - name: 'Async Get Handler', - getHandler: function (name: string) { - return Promise.resolve(handlers[name] || (handlers[name] = createHandler('empty'))); - }, - }, -]; - -scenarios.forEach(function (scenario) { - QUnit.module('Query Params (' + scenario.name + ')', { - beforeEach: function (assert: Assert) { - handlers = {}; - expectedUrl = undefined; - - map(assert, function (match) { - match('/index').to('index'); - match('/parent').to('parent', function (match) { - match('/').to('parentIndex'); - match('/child').to('parentChild'); - }); - }); - }, - }); - - function map(assert: Assert, fn: MatchCallback) { - class QPRouter extends TestRouter { - routeDidChange() {} - routeWillChange() {} - didTransition() {} - willTransition() {} - triggerEvent( - handlerInfos: RouteInfo[], - ignoreFailure: boolean, - name: string, - args: any[] - ) { - trigger(handlerInfos, ignoreFailure, name, ...args); - } - replaceURL(name: string) { - this.updateURL(name); - } - getRoute(name: string) { - return scenario.getHandler(name); - } - getSerializer(): never { - throw new Error('never'); - } - updateURL(newUrl: string) { - if (expectedUrl) { - assert.equal(newUrl, expectedUrl, 'The url is ' + newUrl + ' as expected'); - } - } - } - router = new QPRouter(); - router.map(fn); - } - - function consumeAllFinalQueryParams(params: Dict, finalParams: Dict[]) { - for (let key in params) { - let value = params[key]; - delete params[key]; - finalParams.push({ key: key, value: value }); - } - return true; - } - - QUnit.test( - 'a change in query params fires a queryParamsDidChange event', - async function (assert) { - assert.expect(7); - - let count = 0; - handlers['index'] = createHandler('index', { - setup: function () { - assert.equal( - count, - 0, - "setup should be called exactly once since we're only changing query params after the first transition" - ); - }, - events: { - finalizeQueryParamChange: consumeAllFinalQueryParams, - - queryParamsDidChange: function (changed: Dict, all: Dict) { - switch (count) { - case 0: - assert.ok(false, "shouldn't fire on first trans"); - break; - case 1: - assert.deepEqual(changed, { foo: '5' }); - assert.deepEqual(all, { foo: '5' }); - break; - case 2: - assert.deepEqual(changed, { bar: '6' }); - assert.deepEqual(all, { foo: '5', bar: '6' }); - break; - case 3: - assert.deepEqual(changed, { foo: '8', bar: '9' }); - assert.deepEqual(all, { foo: '8', bar: '9' }); - break; - } - }, - }, - }); - - await router.transitionTo('/index'); - count = 1; - await router.transitionTo('/index?foo=5'); - count = 2; - await router.transitionTo('/index?foo=5&bar=6'); - count = 3; - await router.transitionTo('/index?foo=8&bar=9'); - } - ); - - QUnit.test( - 'transitioning between routes fires a queryParamsDidChange event', - async function (assert) { - assert.expect(8); - let count = 0; - handlers['parent'] = createHandler('parent', { - events: { - finalizeQueryParamChange: consumeAllFinalQueryParams, - queryParamsDidChange: function (changed: Dict, all: Dict) { - switch (count) { - case 0: - assert.ok(false, "shouldn't fire on first trans"); - break; - case 1: - assert.deepEqual(changed, { foo: '5' }); - assert.deepEqual(all, { foo: '5' }); - break; - case 2: - assert.deepEqual(changed, { bar: '6' }); - assert.deepEqual(all, { foo: '5', bar: '6' }); - break; - case 3: - assert.deepEqual(changed, { foo: '8', bar: '9' }); - assert.deepEqual(all, { foo: '8', bar: '9' }); - break; - case 4: - assert.deepEqual(changed, { foo: '10', bar: '11' }); - assert.deepEqual(all, { foo: '10', bar: '11' }); - } - }, - }, - }); - - handlers['parentChild'] = createHandler('parentChild', { - events: { - finalizeQueryParamChange: function () { - // Do nothing since this handler isn't consuming the QPs - return true; - }, - - queryParamsDidChange: function () { - return true; - }, - }, - }); - await router.transitionTo('/parent/child'); - count = 1; - await router.transitionTo('/parent/child?foo=5'); - count = 2; - await router.transitionTo('/parent/child?foo=5&bar=6'); - count = 3; - await router.transitionTo('/parent/child?foo=8&bar=9'); - count = 4; - await router.transitionTo('/parent?foo=10&bar=11'); - } - ); - - QUnit.test( - 'Refreshing the route when changing only query params should correctly set queryParamsOnly', - async function (assert) { - assert.expect(16); - - let initialTransition = true; - - let expectReplace: boolean; - - router.updateURL = function () { - assert.notOk(expectReplace, 'Expected replace but update was called'); - }; - - router.replaceURL = function () { - assert.ok(expectReplace, 'Replace was called but update was expected'); - }; - - handlers['index'] = createHandler('index', { - events: { - finalizeQueryParamChange: function ( - _params: Dict, - _finalParams: Dict[], - transition: Transition - ) { - if (initialTransition) { - assert.notOk( - transition.queryParamsOnly, - 'should not be query params only transition' - ); - initialTransition = false; - } else { - assert.ok(transition.queryParamsOnly, 'should be query params only transition'); - } - }, - - queryParamsDidChange: function () { - router.refresh(); - }, - }, - }); - - handlers['child'] = createHandler('child', { - events: { - finalizeQueryParamChange: function ( - _params: Dict, - _finalParams: Dict, - transition: Transition - ) { - assert.notOk(transition.queryParamsOnly, 'should be normal transition'); - return true; - }, - }, - }); - - expectReplace = false; - - let transition = router.transitionTo('/index'); - assert.notOk( - transition.queryParamsOnly, - 'Initial transition is not query params only transition' - ); - await transition; - - transition = router.transitionTo('/index?foo=123'); - - assert.ok( - transition.queryParamsOnly, - 'Second transition with updateURL intent is query params only' - ); - await ignoreTransitionError(transition); - - expectReplace = true; - transition = router.replaceWith('/index?foo=456'); - assert.ok( - transition.queryParamsOnly, - 'Third transition with replaceURL intent is query params only' - ); - await ignoreTransitionError(transition); - - expectReplace = false; - - transition = router.transitionTo('/parent/child?foo=789'); - assert.notOk( - transition.queryParamsOnly, - 'Fourth transition with transitionTo intent is not query params only' - ); - await transition; - - transition = router.transitionTo('/parent/child?foo=901'); - assert.ok( - transition.queryParamsOnly, - 'Firth transition with transitionTo intent is query params only' - ); - await transition; - - transition = router.transitionTo('/index?foo=123'); - assert.notOk( - transition.queryParamsOnly, - 'Firth transition with transitionTo intent is not query params only' - ); - - return ignoreTransitionError(transition); - } - ); - - QUnit.test( - 'a handler can opt into a full-on transition by calling refresh', - async function (assert) { - assert.expect(3); - - let count = 0; - handlers['index'] = createHandler('index', { - model: function () { - switch (count) { - case 0: - assert.ok(true, 'model called in initial transition'); - break; - case 1: - assert.ok(true, 'model called during refresh'); - break; - case 2: - assert.ok(true, 'model called during refresh w 2 QPs'); - break; - default: - assert.ok(false, "shouldn't have been called for " + count); - } - }, - events: { - queryParamsDidChange: function () { - if (count === 0) { - assert.ok(false, "shouldn't fire on first trans"); - } else { - router.refresh(this as Route); - } - }, - finalizeQueryParamChange: consumeAllFinalQueryParams, - }, - }); - - await router.transitionTo('/index'); - count = 1; - await ignoreTransitionError(router.transitionTo('/index?foo=5')); - count = 2; - return ignoreTransitionError(router.transitionTo('/index?foo=5&wat=lol')); - } - ); - - QUnit.test( - 'at the end of a query param change a finalizeQueryParamChange event is fired', - async function (assert) { - assert.expect(5); - - let eventHandled = false; - let count = 0; - handlers['index'] = createHandler('index', { - setup: function () { - assert.notOk(eventHandled, 'setup should happen before eventHandled'); - }, - events: { - finalizeQueryParamChange: function (all: Dict) { - eventHandled = true; - switch (count) { - case 0: - assert.deepEqual(all, {}); - break; - case 1: - assert.deepEqual(all, { foo: '5' }); - break; - case 2: - assert.deepEqual(all, { foo: '5', bar: '6' }); - break; - case 3: - assert.deepEqual(all, { foo: '8', bar: '9' }); - break; - } - }, - }, - }); - - await router.transitionTo('/index'); - count = 1; - await router.transitionTo('/index?foo=5'); - count = 2; - await router.transitionTo('/index?foo=5&bar=6'); - count = 3; - await router.transitionTo('/index?foo=8&bar=9'); - } - ); - - QUnit.test( - 'failing to consume QPs in finalize event tells the router it no longer has those params', - async function (assert) { - assert.expect(2); - - handlers['index'] = createHandler('index', { - setup: function () { - assert.ok(true, 'setup was entered'); - }, - }); - - await router.transitionTo('/index?foo=8&bar=9'); - - assert.deepEqual(router.state!.queryParams, {}); - } - ); - - QUnit.test( - 'consuming QPs in finalize event tells the router those params are active', - async function (assert) { - assert.expect(1); - - handlers['index'] = createHandler('index', { - events: { - finalizeQueryParamChange: function (params: Dict, finalParams: Dict[]) { - finalParams.push({ key: 'foo', value: params['foo'] }); - }, - }, - }); - - await router.transitionTo('/index?foo=8&bar=9'); - assert.deepEqual(router.state!.queryParams, { foo: '8' }); - } - ); - - QUnit.test( - "can hide query params from URL if they're marked as visible=false in finalizeQueryParamChange", - async function (assert) { - assert.expect(2); - - handlers['index'] = createHandler('index', { - events: { - finalizeQueryParamChange: function (params: Dict, finalParams: Dict[]) { - finalParams.push({ key: 'foo', value: params['foo'], visible: false }); - finalParams.push({ key: 'bar', value: params['bar'] }); - }, - }, - }); - - expectedUrl = '/index?bar=9'; - await router.transitionTo('/index?foo=8&bar=9'); - assert.deepEqual(router.state!.queryParams, { foo: '8', bar: '9' }); - } - ); - - QUnit.test('transitionTo() works with single query param arg', async function (assert) { - assert.expect(2); - - handlers['index'] = createHandler('index', { - events: { - finalizeQueryParamChange: function (params: Dict, finalParams: Dict[]) { - finalParams.push({ key: 'foo', value: params['foo'] }); - finalParams.push({ key: 'bar', value: params['bar'] }); - }, - }, - }); - - await router.transitionTo('/index?bar=9&foo=8'); - assert.deepEqual(router.state!.queryParams, { foo: '8', bar: '9' }); - - expectedUrl = '/index?foo=123'; - await router.transitionTo({ queryParams: { foo: '123' } }); - }); - - QUnit.test( - 'handleURL will NOT follow up with a replace URL if query params are already in sync', - function (assert) { - assert.expect(0); - - router.replaceURL = function (url) { - assert.ok(false, "query params are in sync, this replaceURL shouldn't happen: " + url); - }; - - router.handleURL('/index'); - } - ); - - QUnit.test('model hook receives queryParams', async function (assert) { - assert.expect(1); - - handlers['index'] = createHandler('index', { - model: function (params: Dict) { - assert.deepEqual(params, { queryParams: { foo: '5' } }); - }, - }); - - await router.transitionTo('/index?foo=5'); - }); - - QUnit.test( - 'can cause full transition by calling refresh within queryParamsDidChange', - async function (assert) { - assert.expect(5); - - let modelCount = 0; - handlers['index'] = createHandler('index', { - model: function (params: Dict) { - ++modelCount; - if (modelCount === 1) { - assert.deepEqual(params, { queryParams: { foo: '5' } }); - } else if (modelCount === 2) { - assert.deepEqual(params, { queryParams: { foo: '6' } }); - } - }, - events: { - queryParamsDidChange: function () { - router.refresh(this as Route); - }, - }, - }); - - assert.equal(modelCount, 0); - await ignoreTransitionError(router.transitionTo('/index?foo=5')); - assert.equal(modelCount, 1); - await ignoreTransitionError(router.transitionTo('/index?foo=6')); - assert.equal(modelCount, 2); - } - ); - - QUnit.test('can retry a query-params refresh', async function (assert) { - let causeRedirect = false; - - map(assert, function (match) { - match('/index').to('index'); - match('/login').to('login'); - }); - - assert.expect(11); - - let redirect = false; - let indexTransition: Transition; - handlers['index'] = createHandler('index', { - model: function (_params: Dict, transition: Transition) { - if (redirect) { - indexTransition = transition; - router.transitionTo('login'); - } - }, - setup: function () { - assert.ok(true, 'index#setup'); - }, - events: { - queryParamsDidChange: function () { - assert.ok(true, 'index#queryParamsDidChange'); - redirect = causeRedirect; - router.refresh(this as Route); - }, - finalizeQueryParamChange: function (params: Dict, finalParams: Dict[]) { - (finalParams as any).foo = params['foo']; // TODO wat - finalParams.push({ key: 'foo', value: params['foo'] }); - }, - }, - }); - - handlers['login'] = createHandler('login', { - setup: function () { - assert.ok(true, 'login#setup'); - }, - }); - - expectedUrl = '/index?foo=abc'; - await ignoreTransitionError(router.transitionTo('/index?foo=abc')); - causeRedirect = true; - expectedUrl = '/login'; - await ignoreTransitionError(router.transitionTo('/index?foo=def')); - causeRedirect = false; - redirect = false; - assert.ok(indexTransition!, 'index transition was saved'); - expectedUrl = '/index?foo=def'; - return ignoreTransitionError(indexTransition!.retry()); - }); - - QUnit.test( - 'tests whether query params to transitionTo are considered active', - async function (assert) { - assert.expect(6); - - handlers['index'] = createHandler('index', { - events: { - finalizeQueryParamChange: function (params: Dict, finalParams: Dict[]) { - finalParams.push({ key: 'foo', value: params['foo'] }); - finalParams.push({ key: 'bar', value: params['bar'] }); - }, - }, - }); - - await router.transitionTo('/index?foo=8&bar=9'); - assert.deepEqual(router.state!.queryParams, { foo: '8', bar: '9' }); - assert.ok( - router.isActive('index', { queryParams: { foo: '8', bar: '9' } }), - 'The index handler is active' - ); - assert.ok( - router.isActive('index', { queryParams: { foo: 8, bar: 9 } }), - 'Works when property is number' - ); - assert.notOk( - router.isActive('index', { queryParams: { foo: '9' } }), - 'Only supply one changed query param' - ); - assert.notOk( - router.isActive('index', { - queryParams: { foo: '8', bar: '10', baz: '11' }, - }), - 'A new query param was added' - ); - assert.notOk( - router.isActive('index', { queryParams: { foo: '8', bar: '11' } }), - 'A query param changed' - ); - } - ); - - QUnit.test( - 'tests whether array query params to transitionTo are considered active', - async function (assert) { - assert.expect(7); - - handlers['index'] = createHandler('index', { - events: { - finalizeQueryParamChange: function (params: Dict, finalParams: Dict[]) { - finalParams.push({ key: 'foo', value: params['foo'] }); - }, - }, - }); - - await router.transitionTo('/index?foo[]=1&foo[]=2'); - assert.deepEqual(router.state!.queryParams, { foo: ['1', '2'] }); - assert.ok( - router.isActive('index', { queryParams: { foo: ['1', '2'] } }), - 'The index handler is active' - ); - assert.ok( - router.isActive('index', { queryParams: { foo: [1, 2] } }), - 'Works when array has numeric elements' - ); - assert.notOk(router.isActive('index', { queryParams: { foo: ['2', '1'] } }), 'Change order'); - assert.notOk( - router.isActive('index', { queryParams: { foo: ['1', '2', '3'] } }), - 'Change Length' - ); - assert.notOk( - router.isActive('index', { queryParams: { foo: ['3', '4'] } }), - 'Change Content' - ); - assert.notOk(router.isActive('index', { queryParams: { foo: [] } }), 'Empty Array'); - } - ); -}); diff --git a/packages/router_js/tests/route_info_test.ts b/packages/router_js/tests/route_info_test.ts deleted file mode 100644 index 31ba500e7b7..00000000000 --- a/packages/router_js/tests/route_info_test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type { Transition } from '../index'; -import type { Dict } from '../lib/core'; -import type { IModel, Route } from '../lib/route-info'; -import { - ResolvedRouteInfo, - toReadOnlyRouteInfo, - UnresolvedRouteInfoByObject, - UnresolvedRouteInfoByParam, -} from '../lib/route-info'; -import InternalTransition from '../lib/transition'; -import URLTransitionIntent from '../lib/transition-intent/url-transition-intent'; -import { resolve } from 'rsvp'; -import { createHandler, createHandlerInfo, TestRouter } from './test_helpers'; - -QUnit.module('RouteInfo'); - -QUnit.test('ResolvedRouteInfo resolve to themselves', function (assert) { - assert.expect(1); - let router = new TestRouter(); - let routeInfo = new ResolvedRouteInfo(router, 'foo', [], {}, createHandler('empty')); - let intent = new URLTransitionIntent(router, 'foo'); - - let transition = new InternalTransition(router, intent, undefined); - - routeInfo.resolve(transition).then((resolvedRouteInfo) => { - assert.equal(routeInfo, resolvedRouteInfo); - }); -}); - -QUnit.test('UnresolvedRouteInfoByParam defaults params to {}', function (assert) { - let router = new TestRouter(); - let routeInfo = new UnresolvedRouteInfoByParam(router, 'empty', [], {}); - assert.deepEqual(routeInfo.params, {}); - - let routeInfo2 = new UnresolvedRouteInfoByParam(router, 'empty', [], { foo: 5 }); - assert.deepEqual(routeInfo2.params, { foo: 5 }); -}); - -QUnit.test('RouteInfo can be aborted mid-resolve', function (assert) { - assert.expect(1); - - let routeInfo = createHandlerInfo('stub'); - - let transition = {} as Transition; - transition.isAborted = true; - - routeInfo - .resolve(transition) - .then(() => { - assert.ok(false, 'unreachable'); - }) - .catch((e) => { - assert.equal(e.message, 'TransitionAborted'); - }); -}); - -QUnit.test('RouteInfo#resolve resolves with a ResolvedRouteInfo', function (assert) { - assert.expect(1); - - let routeInfo = createHandlerInfo('stub'); - routeInfo.resolve({} as Transition).then((resolvedRouteInfo) => { - assert.ok(resolvedRouteInfo instanceof ResolvedRouteInfo); - }); -}); - -QUnit.test('RouteInfo#resolve runs beforeModel hook on handler', function (assert) { - assert.expect(1); - - let transition = {} as Transition; - - let routeInfo = createHandlerInfo('stub', { - route: createHandler('stub', { - beforeModel: function (currentTransition: Transition) { - assert.equal( - transition, - currentTransition, - 'beforeModel was called with the payload we passed to resolve()' - ); - }, - }), - }); - - routeInfo.resolve(transition).then(() => { - assert.ok(true, 'routeInfo resolved successfully'); - }); -}); - -QUnit.test('RouteInfo#resolve runs getModel hook', async function (assert) { - assert.expect(2); - - let transition = {} as Transition; - - let routeInfo = createHandlerInfo('stub', { - getModel(payload: Dict) { - assert.equal(payload, transition); - }, - }); - - routeInfo.resolve(transition).then(() => { - assert.ok(true, 'routeInfo resolved successfully'); - }); -}); - -/** - * This test file was not being run before it was integrated from upstream and a number of these - * tests were failing as soon as we started running it again. - * - * This test has some strange timing issues with the strange backburner wrapper it's doing in the - * test-helpers. We could not figure this out and really the solution should be to remove the strange - * wrapper. - * - * TODO: unskip this test - */ -QUnit.skip('RouteInfo#resolve runs afterModel hook on handler', function (assert) { - assert.expect(3); - - let transition = {} as Transition; - let model = {}; - - let routeInfo = createHandlerInfo('foo', { - route: createHandler('foo', { - afterModel(resolvedModel: Dict, payload: Dict) { - assert.equal(resolvedModel, model, 'afterModel receives the value resolved by model'); - assert.equal(payload, transition); - return resolve(123); // 123 should get ignored - }, - }), - getModel() { - return resolve(model); - }, - }); - - routeInfo.resolve(transition).then((resolvedRouteInfo) => { - assert.equal(resolvedRouteInfo.context, model, 'RouteInfo resolved with correct model'); - }); -}); - -QUnit.test('UnresolvedRouteInfoByParam gets its model hook called', function (assert) { - assert.expect(2); - let router = new TestRouter(); - - let transition = {} as Transition; - - let routeInfo = new UnresolvedRouteInfoByParam( - router, - 'empty', - [], - { first_name: 'Alex', last_name: 'Matchnerd' }, - createHandler('h', { - model(params: Dict, payload: Dict) { - assert.equal(payload, transition); - assert.deepEqual(params, { - first_name: 'Alex', - last_name: 'Matchnerd', - }); - }, - }) - ); - - routeInfo.resolve(transition); -}); - -QUnit.test('UnresolvedRouteInfoByObject does NOT get its model hook called', function (assert) { - type Dorkleton = { name: string } & IModel; - - assert.expect(1); - - class TestRouteInfo extends UnresolvedRouteInfoByObject> { - __routeHandler?: Route; - get route(): Route { - if (this.__routeHandler) { - return this.__routeHandler; - } - return (this.__routeHandler = createHandler('unresolved', { - model: function () { - assert.ok(false, "I shouldn't be called because I already have a context/model"); - }, - })); - } - set route(_value) { - // TODO: this stub is here because something is setting this and it breaks if there isn't a setter - } - } - - let routeInfo = new TestRouteInfo( - new TestRouter(), - 'unresolved', - ['wat'], - resolve({ name: 'dorkletons' }) - ); - - routeInfo.resolve({} as Transition).then((resolvedRouteInfo) => { - assert.equal(resolvedRouteInfo.context!.name, 'dorkletons'); - }); -}); - -QUnit.test('RouteInfo.find', function (assert) { - assert.expect(3); - let router = new TestRouter(); - let parent = new ResolvedRouteInfo(router, 'parent', [], {}, createHandler('parent')); - let child = new ResolvedRouteInfo(router, 'child', [], {}, createHandler('child')); - let grandChild = new ResolvedRouteInfo(router, 'grandChild', [], {}, createHandler('grandChild')); - let [root] = toReadOnlyRouteInfo([parent, child, grandChild]); - - enum RouteInfoNames { - parent, - child, - grandChild, - } - - root!.find((routInfo, i) => { - assert.equal(RouteInfoNames[i], routInfo.name); - return false; - }); -}); - -QUnit.test('RouteInfo.find returns matched', function (assert) { - assert.expect(3); - let router = new TestRouter(); - let parent = new ResolvedRouteInfo(router, 'parent', [], {}, createHandler('parent')); - let child = new ResolvedRouteInfo(router, 'child', [], {}, createHandler('child')); - let grandChild = new ResolvedRouteInfo(router, 'grandChild', [], {}, createHandler('grandChild')); - let [root] = toReadOnlyRouteInfo([parent, child, grandChild]); - - enum RouteInfoNames { - parent, - child, - grandChild, - } - - let childInfo = root!.find((routInfo, i) => { - assert.equal(RouteInfoNames[i], routInfo.name); - return routInfo.name === 'child'; - }); - assert.equal(childInfo!.name, 'child'); -}); diff --git a/packages/router_js/tests/router_test.ts b/packages/router_js/tests/router_test.ts deleted file mode 100644 index 2f8d320a751..00000000000 --- a/packages/router_js/tests/router_test.ts +++ /dev/null @@ -1,6629 +0,0 @@ -/* eslint-disable qunit/no-conditional-assertions, qunit/no-assert-logical-expression, qunit/no-early-return, no-console, no-throw-literal */ -import type { MatchCallback } from 'route-recognizer'; -import type { Route, Transition } from '../index'; -import type Router from '../index'; -import type { Dict, Maybe } from '../lib/core'; -import type { - IModel, - RouteInfo as PublicRouteInfo, - RouteInfoWithAttributes, -} from '../lib/route-info'; -import type RouteInfo from '../lib/route-info'; -import type { SerializerFunc } from '../lib/router'; -import { logAbort, PARAMS_SYMBOL, QUERY_PARAMS_SYMBOL, STATE_SYMBOL } from '../lib/transition'; -import type { TransitionError } from '../lib/transition-state'; -import { Promise, reject } from 'rsvp'; -import { - assertAbort, - createHandler, - isExiting, - replaceWith, - shouldNotHappen, - TestRouter, - transitionToWithAbort, - trigger, - ignoreTransitionError, -} from './test_helpers'; - -let router: Router; -let url: string | undefined; -let routes: Dict; - -function isPresent(maybe: Maybe): maybe is PublicRouteInfo { - return maybe !== undefined && maybe !== null; -} - -let serializers: Dict>, expectedUrl: Maybe; -let scenarios = [ - { - name: 'Sync Get Handler', - async: false, - getRoute: function (name: string) { - return routes[name] || (routes[name] = createHandler('empty')); - }, - getSerializer: function (_name: string) { - return undefined; - }, - }, - { - name: 'Async Get Handler', - async: true, - getRoute: function (name: string) { - // Treat 'loading' route transitions are synchronous - let handler = routes[name] || (routes[name] = createHandler('empty')); - return name === 'loading' ? handler : Promise.resolve(handler); - }, - getSerializer: function (name: string) { - return serializers && serializers[name]; - }, - }, -]; - -scenarios.forEach(function (scenario) { - QUnit.module('The router (' + scenario.name + ')', { - beforeEach: function (assert: Assert) { - routes = {}; - expectedUrl = undefined; - url = undefined; - - map(assert, function (match) { - match('/index').to('index'); - match('/about').to('about'); - match('/faq').to('faq'); - match('/nested').to('nestedParent', function (match) { - match('/').to('nestedChild'); - }); - match('/posts', function (match) { - match('/:id').to('showPost'); - match('/:postId/:commentId').to('showComment'); - match('/on/:date').to('showPostsForDate'); - match('/admin/:id').to('admin', function (match) { - match('/posts').to('adminPosts'); - match('/posts/:post_id').to('adminPost'); - }); - match('/').to('postIndex', function (match) { - match('/all').to('showAllPosts'); - - // TODO: Support canonical: true - match('/').to('showAllPosts'); - match('/popular').to('showPopularPosts'); - match('/filter/:filter_id').to('showFilteredPosts'); - }); - }); - }); - }, - }); - - function map(assert: Assert, fn: MatchCallback) { - class Router extends TestRouter { - routeDidChange() {} - routeWillChange() {} - didTransition() {} - willTransition() {} - replaceURL(name: string) { - this.updateURL(name); - } - triggerEvent( - handlerInfos: RouteInfo[], - ignoreFailure: boolean, - name: string, - args: any[] - ) { - trigger(handlerInfos, ignoreFailure, name, ...args); - } - - getRoute(name: string) { - return scenario.getRoute(name); - } - - getSerializer(name: string) { - return scenario.getSerializer(name); - } - - updateURL(newUrl: string) { - if (expectedUrl) { - assert.equal(newUrl, expectedUrl, 'The url is ' + newUrl + ' as expected'); - } - - url = newUrl; - } - } - - router = new Router(); - - router.map(fn); - } - - QUnit.test('Mapping adds named routes to the end', function (assert) { - url = router.recognizer.generate('showPost', { id: 1 }); - assert.equal(url, '/posts/1'); - - url = router.recognizer.generate('showAllPosts'); - assert.equal(url, '/posts'); - - url = router.recognizer.generate('showComment', { - postId: 1, - commentId: 2, - }); - assert.equal(url, '/posts/1/2'); - - url = router.generate('showComment', 1, 2); - assert.equal(url, '/posts/1/2'); - }); - - QUnit.test('Handling an invalid URL returns a rejecting promise', function (assert) { - router.handleURL('/unknown').then(shouldNotHappen(assert), function (e: Error) { - assert.equal(e.name, 'UnrecognizedURLError', 'error.name is UnrecognizedURLError'); - }); - }); - - function routePath(infos: RouteInfo[]) { - let path = []; - - for (let i = 0, l = infos.length; i < l; i++) { - path.push(infos[i]!.name); - } - - return path.join('.'); - } - - QUnit.test( - 'Handling a URL triggers model on the handler and passes the result into the setup method', - function (assert) { - assert.expect(4); - - let post = { post: true }; - - routes = { - showPost: createHandler('showPost', { - model: function (params: Dict) { - assert.deepEqual( - params, - { id: '1', queryParams: {} }, - 'showPost#model called with id 1' - ); - return post; - }, - - setup: function (object: Dict) { - assert.strictEqual(object, post, 'setup was called with expected model'); - assert.equal( - routes['showPost']!.context, - post, - 'context was properly set on showPost handler' - ); - }, - }), - }; - - router.didTransition = function (infos) { - assert.equal(routePath(infos), 'showPost'); - }; - - router.handleURL('/posts/1'); - } - ); - - QUnit.test('isActive should not break on initial intermediate route', function (assert) { - assert.expect(1); - router.intermediateTransitionTo('/posts/admin/1/posts'); - assert.ok(router.isActive('admin', '1')); - }); - - QUnit.test('Handling a URL passes in query params', async function (assert) { - assert.expect(3); - - routes = { - index: createHandler('index', { - model: function (_params: string[], transition: Transition) { - assert.deepEqual(transition[QUERY_PARAMS_SYMBOL], { - sort: 'date', - filter: 'true', - }); - }, - events: { - finalizeQueryParamChange: function (params: string[], finalParams: Dict[]) { - assert.ok(true, 'finalizeQueryParamChange'); - // need to consume the params so that the router - // knows that they're active - finalParams.push({ key: 'sort', value: params['sort'] }); - finalParams.push({ key: 'filter', value: params['filter'] }); - }, - }, - }), - }; - - await router.handleURL('/index?sort=date&filter'); - assert.deepEqual(router.state!.queryParams, { - sort: 'date', - filter: 'true', - }); - }); - - QUnit.test('handleURL accepts slash-less URLs', function (assert) { - assert.expect(1); - - routes = { - showAllPosts: createHandler('showAllPosts', { - setup: function () { - assert.ok(true, "showAllPosts' setup called"); - }, - }), - }; - - router.handleURL('posts/all'); - }); - - QUnit.test('handleURL accepts query params', function (assert) { - assert.expect(1); - - routes = { - showAllPosts: createHandler('showAllPosts', { - setup: function () { - assert.ok(true, "showAllPosts' setup called"); - }, - }), - }; - - router.handleURL('/posts/all?sort=name&sortDirection=descending'); - }); - - QUnit.test("redirect hook shouldn't get called on parent routes", async function (assert) { - map(assert, function (match) { - match('/').to('app', function (match) { - match('/').to('index'); - match('/other').to('other'); - }); - }); - - let appRedirects = 0; - routes = { - app: createHandler('app', { - redirect: function () { - appRedirects++; - }, - }), - }; - - await router.transitionTo('/'); - assert.equal(appRedirects, 1); - await router.transitionTo('other'); - assert.equal(appRedirects, 1); - }); - - QUnit.test( - 'when transitioning with the same context, setup should only be called once', - async function (assert) { - let parentSetupCount = 0, - childSetupCount = 0; - - let context = { id: 1 }; - - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post', function (match) { - match('/details').to('postDetails'); - }); - }); - - routes = { - post: createHandler('post', { - setup: function () { - parentSetupCount++; - }, - }), - - postDetails: createHandler('postDetails', { - setup: function () { - childSetupCount++; - }, - }), - }; - - await router.transitionTo('/'); - - assert.equal(parentSetupCount, 0, 'precondition - parent not setup'); - assert.equal(childSetupCount, 0, 'precondition - child not setup'); - - await router.transitionTo('postDetails', context); - - assert.equal(parentSetupCount, 1, 'after initial transition parent is setup once'); - assert.equal(childSetupCount, 1, 'after initial transition child is setup once'); - - await router.transitionTo('postDetails', context); - - assert.equal(parentSetupCount, 1, 'after duplicate transition, parent is still setup once'); - assert.equal(childSetupCount, 1, 'after duplicate transition, child is still setup once'); - } - ); - - QUnit.test('basic route change events', function (assert) { - assert.expect(11); - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post', function (match) { - match('/details').to('postDetails'); - }); - }); - - let enteredWillChange = 0; - let enteredDidChange = 0; - routes = { - post: createHandler('post', { - model() { - return { title: 'The Title' }; - }, - }), - postDetails: createHandler('postDetails', { - model() { - return { body: 'The Content' }; - }, - }), - }; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - - if (isPresent(transition.to)) { - assert.equal(transition.to.localName, 'postDetails'); - assert.equal(transition.from, null); - assert.equal(transition.to.parent!.localName, 'post'); - assert.equal((transition.to as any).attributes, undefined); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - let to = transition.to! as RouteInfoWithAttributes; - if (isPresent(transition.to)) { - assert.equal(to.localName, 'postDetails'); - assert.equal(transition.from!, null); - assert.equal(to.parent!.localName, 'post'); - assert.deepEqual(to.attributes, { body: 'The Content' }); - assert.deepEqual((to.parent! as RouteInfoWithAttributes).attributes, { - title: 'The Title', - }); - } - }; - - router.transitionTo('/posts/1/details').then(() => { - assert.equal(enteredWillChange, 1); - assert.equal(enteredDidChange, 1); - }); - }); - - QUnit.test('basic events with route metadata', function (assert) { - assert.expect(10); - map(assert, function (match) { - match('/').to('index'); - match('/profile').to('profile'); - match('/posts/:id').to('post', function (match) { - match('/details').to('postDetails'); - }); - }); - - routes = { - post: createHandler('post', { - buildRouteInfoMetadata() { - return 'post-page'; - }, - model() { - return { title: 'The Title' }; - }, - }), - profile: createHandler('profile', { - buildRouteInfoMetadata() { - return 'profile-page'; - }, - }), - postDetails: createHandler('postDetails', { - buildRouteInfoMetadata() { - return 'post-details-page'; - }, - model() { - return { body: 'The Content' }; - }, - }), - }; - - router.routeWillChange = (transition: Transition) => { - if (!isPresent(transition.from) && isPresent(transition.to)) { - if (scenario.async) { - assert.equal(transition.to.metadata, null, 'initial to leaf'); - assert.equal(transition.to.parent!.metadata, null, 'initial to leaf'); - } else { - assert.equal(transition.to.metadata, 'post-details-page'); - assert.equal(transition.to.parent!.metadata, 'post-page'); - } - } - - if (isPresent(transition.from) && isPresent(transition.to)) { - if (scenario.async) { - assert.equal(transition.from.metadata, 'post-details-page', 'from leaf'); - assert.equal(transition.from.parent!.metadata, 'post-page', 'from parent'); - assert.equal(transition.to.metadata, null, 'to leaf'); - } else { - assert.equal(transition.from.metadata, 'post-details-page'); - assert.equal(transition.from.parent!.metadata, 'post-page'); - assert.equal(transition.to.metadata, 'profile-page'); - } - } - }; - - router.routeDidChange = (transition: Transition) => { - if (!isPresent(transition.from) && isPresent(transition.to)) { - assert.equal(transition.to.metadata, 'post-details-page', 'initial to leaf'); - assert.equal(transition.to.parent!.metadata, 'post-page', 'initial to parent'); - } - - if (isPresent(transition.from) && isPresent(transition.to)) { - assert.equal(transition.from.metadata, 'post-details-page', 'from: /profile visited'); - assert.equal( - transition.from.parent!.metadata, - 'post-page', - 'from: /profile visited parent' - ); - assert.equal(transition.to.metadata, 'profile-page', 'to: /profile'); - } - }; - - router.transitionTo('/posts/1/details').then(() => { - return router.transitionTo('/profile'); - }); - }); - - QUnit.test('basic route change events with replacement', function (assert) { - assert.expect(14); - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post', function (match) { - match('/details').to('postDetails'); - }); - match('/post-details/:id').to('canonicalPostDetails'); - }); - - let enteredWillChange = 0; - let enteredDidChange = 0; - routes = { - post: createHandler('post'), - postDetails: createHandler('postDetails'), - canonicalPostDetails: createHandler('canonicalPostDetails'), - }; - - let replacement = false; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (isPresent(transition.to)) { - if (replacement) { - assert.equal(transition.to.localName, 'canonicalPostDetails'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'postDetails'); - assert.equal(transition.to!.parent!, null); - } else { - assert.equal(transition.to!.localName, 'postDetails'); - assert.equal(transition.from!, null); - assert.equal(transition.to!.parent!.localName, 'post'); - } - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (isPresent(transition.to)) { - if (replacement) { - assert.equal(transition.to!.localName, 'canonicalPostDetails'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'postDetails'); - assert.equal(transition.to!.parent, null); - } else { - assert.equal(transition.to!.localName, 'postDetails'); - assert.equal(transition.from!, null); - assert.equal(transition.to!.parent!.localName, 'post'); - } - } - }; - - router - .transitionTo('/posts/1/details') - .then(() => { - replacement = true; - return router.replaceWith('/post-details/1'); - }) - .then(() => { - assert.equal(enteredWillChange, 2); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('basic route change events with nested replacement', function (assert) { - assert.expect(12); - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post', function (match) { - match('/details').to('postDetails'); - }); - match('/post-details/:id').to('canonicalPostDetails'); - }); - - let enteredWillChange = 0; - let enteredDidChange = 0; - routes = { - post: createHandler('post'), - postDetails: createHandler('postDetails', { - model() { - router.replaceWith('/post-details/1'); - replacement = true; - }, - }), - canonicalPostDetails: createHandler('canonicalPostDetails'), - }; - - let replacement = false; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (isPresent(transition.to)) { - if (replacement) { - assert.equal(transition.to.localName, 'canonicalPostDetails'); - assert.equal(transition.from!, null); - assert.equal(transition.to.parent!, null); - } else { - assert.equal(transition.to.localName, 'postDetails'); - assert.equal(transition.from!, null); - assert.equal(transition.to.parent!.localName, 'post'); - replacement = true; - } - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - assert.equal(transition.to!.localName, 'canonicalPostDetails'); - assert.equal(transition.from!, null); - assert.equal(transition.to!.parent, null); - }; - - router - .transitionTo('/posts/1/details') - .catch((err: any) => { - assert.equal(err.name, 'TransitionAborted'); - return router.activeTransition as any; - }) - .then(() => { - assert.equal(enteredWillChange, 2); - assert.equal(enteredDidChange, 1); - }); - }); - - QUnit.test('basic route change events with params', function (assert) { - assert.expect(26); - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post'); - }); - - let enteredWillChange = 0; - let enteredDidChange = 0; - routes = { - index: createHandler('index', { - model() { - return Promise.resolve('Index'); - }, - }), - post: createHandler('post', { - model(params: Dict) { - return Promise.resolve(params['id']); - }, - }), - }; - - let newParam = false; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - assert.deepEqual(transition.to!.paramNames, ['id']); - if (newParam) { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'post'); - assert.deepEqual(isPresent(transition.from) && transition.from.attributes, '1'); - assert.deepEqual(transition.to!.params, { id: '2' }); - assert.equal(url, '/posts/1'); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(transition.from, null); - assert.notOk(url); - assert.deepEqual(transition.to!.params, { id: '1' }); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - let to = transition.to! as RouteInfoWithAttributes; - assert.deepEqual(transition.to!.paramNames, ['id']); - if (newParam) { - assert.equal(to.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'post'); - assert.deepEqual(to.params, { id: '2' }); - assert.deepEqual(to.attributes, '2'); - assert.deepEqual(isPresent(transition.from) && transition.from.attributes, '1'); - assert.equal(url, '/posts/2'); - } else { - assert.equal(to.localName, 'post'); - assert.equal(transition.from, null); - assert.equal(url, '/posts/1'); - assert.deepEqual(to.params, { id: '1' }); - assert.deepEqual(to.attributes, '1'); - } - }; - - router - .transitionTo('/posts/1') - .then(() => { - newParam = true; - return router.transitionTo('/posts/2'); - }) - .then(() => { - assert.equal(enteredWillChange, 2); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('top-level recognizeAndLoad url', function (assert) { - map(assert, function (match) { - match('/').to('index'); - }); - - routes = { - index: createHandler('index', { - model() { - return { name: 'index', data: 1 }; - }, - }), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - router.recognizeAndLoad('/').then((routeInfoWithAttributes: RouteInfoWithAttributes) => { - assert.notOk(router.activeTransition, 'Does not create an active transition'); - if (routeInfoWithAttributes === null) { - assert.ok(false); - return; - } - assert.equal(routeInfoWithAttributes.name, 'index'); - assert.equal(routeInfoWithAttributes.localName, 'index'); - assert.equal(routeInfoWithAttributes.parent, null); - assert.equal(routeInfoWithAttributes.child, null); - assert.deepEqual(routeInfoWithAttributes.attributes, { name: 'index', data: 1 }); - assert.deepEqual(routeInfoWithAttributes.queryParams, {}); - assert.deepEqual(routeInfoWithAttributes.params, {}); - assert.deepEqual(routeInfoWithAttributes.paramNames, []); - }); - }); - - QUnit.test('top-level parameterized recognizeAndLoad', function (assert) { - map(assert, function (match) { - match('/posts/:id').to('posts'); - }); - - routes = { - posts: createHandler('posts', { - model(params: { id: string }) { - return { name: 'posts', data: params['id'] }; - }, - }), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - router - .recognizeAndLoad('/posts/123') - .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { - assert.notOk(router.activeTransition, 'Does not create an active transition'); - if (routeInfoWithAttributes === null) { - assert.ok(false); - return; - } - assert.equal(routeInfoWithAttributes.name, 'posts'); - assert.equal(routeInfoWithAttributes.localName, 'posts'); - assert.equal(routeInfoWithAttributes.parent, null); - assert.equal(routeInfoWithAttributes.child, null); - assert.deepEqual(routeInfoWithAttributes.attributes, { name: 'posts', data: '123' }); - assert.deepEqual(routeInfoWithAttributes.queryParams, {}); - assert.deepEqual(routeInfoWithAttributes.params, { id: '123' }); - assert.deepEqual(routeInfoWithAttributes.paramNames, ['id']); - }); - }); - - QUnit.test('nested recognizeAndLoad', function (assert) { - routes = { - postIndex: createHandler('postIndex'), - showPopularPosts: createHandler('showPopularPosts', { - model() { - return { name: 'showPopularPosts', data: 123 }; - }, - }), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - router - .recognizeAndLoad('/posts/popular') - .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { - assert.notOk(router.activeTransition, 'Does not create an active transition'); - if (routeInfoWithAttributes === null) { - assert.ok(false); - return; - } - assert.equal(routeInfoWithAttributes.name, 'showPopularPosts'); - assert.equal(routeInfoWithAttributes.localName, 'showPopularPosts'); - assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); - assert.equal(routeInfoWithAttributes.child, null); - assert.deepEqual(routeInfoWithAttributes.attributes, { - name: 'showPopularPosts', - data: 123, - }); - assert.deepEqual(routeInfoWithAttributes.queryParams, {}); - assert.deepEqual(routeInfoWithAttributes.params, {}); - assert.deepEqual(routeInfoWithAttributes.paramNames, []); - }); - }); - - QUnit.test('nested params recognizeAndLoad', function (assert) { - routes = { - postIndex: createHandler('postIndex'), - showFilteredPosts: createHandler('showFilteredPosts', { - model(params: { filter_id: string }) { - return { name: 'showFilteredPosts', data: params['filter_id'] }; - }, - }), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - router - .recognizeAndLoad('/posts/filter/1') - .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { - assert.notOk(router.activeTransition, 'Does not create an active transition'); - if (routeInfoWithAttributes === null) { - assert.ok(false); - return; - } - assert.equal(routeInfoWithAttributes.name, 'showFilteredPosts'); - assert.equal(routeInfoWithAttributes.localName, 'showFilteredPosts'); - assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); - assert.equal(routeInfoWithAttributes.child, null); - assert.deepEqual(routeInfoWithAttributes.attributes, { - name: 'showFilteredPosts', - data: '1', - }); - assert.deepEqual(routeInfoWithAttributes.queryParams, {}); - assert.deepEqual(routeInfoWithAttributes.params, { filter_id: '1' }); - assert.deepEqual(routeInfoWithAttributes.paramNames, ['filter_id']); - }); - }); - - QUnit.test('top-level QPs recognizeAndLoad', function (assert) { - routes = { - showAllPosts: createHandler('showAllPosts', { - model() { - return { name: 'showAllPosts', data: 'qp' }; - }, - }), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - router - .recognizeAndLoad('/posts/?a=b') - .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { - assert.notOk(router.activeTransition, 'Does not create an active transition'); - if (routeInfoWithAttributes === null) { - assert.ok(false); - return; - } - assert.equal(routeInfoWithAttributes.name, 'showAllPosts'); - assert.equal(routeInfoWithAttributes.localName, 'showAllPosts'); - assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); - assert.equal(routeInfoWithAttributes.child, null); - assert.deepEqual(routeInfoWithAttributes.attributes, { - name: 'showAllPosts', - data: 'qp', - }); - assert.deepEqual(routeInfoWithAttributes.queryParams, { a: 'b' }); - assert.deepEqual(routeInfoWithAttributes.params, {}); - assert.deepEqual(routeInfoWithAttributes.paramNames, []); - }); - }); - - QUnit.test('top-level params and QPs recognizeAndLoad', function (assert) { - routes = { - postsIndex: createHandler('postsIndex'), - showFilteredPosts: createHandler('showFilteredPosts', { - model(params: { filter_id: string }) { - return { name: 'showFilteredPosts', data: params['filter_id'] }; - }, - }), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - router - .recognizeAndLoad('/posts/filter/123?a=b') - .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { - assert.notOk(router.activeTransition, 'Does not create an active transition'); - if (routeInfoWithAttributes === null) { - assert.ok(false); - return; - } - assert.equal(routeInfoWithAttributes.name, 'showFilteredPosts'); - assert.equal(routeInfoWithAttributes.localName, 'showFilteredPosts'); - assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); - assert.equal(routeInfoWithAttributes.child, null); - assert.deepEqual(routeInfoWithAttributes.attributes, { - name: 'showFilteredPosts', - data: '123', - }); - assert.deepEqual(routeInfoWithAttributes.queryParams, { a: 'b' }); - assert.deepEqual(routeInfoWithAttributes.params, { filter_id: '123' }); - assert.deepEqual(routeInfoWithAttributes.paramNames, ['filter_id']); - }); - }); - - QUnit.test('unrecognized url rejects', function (assert) { - router.recognizeAndLoad('/fixzzz').then( - () => { - assert.ok(false, 'never here'); - }, - (reason: string) => { - assert.equal(reason, `URL /fixzzz was not recognized`); - } - ); - }); - - QUnit.test('top-level recognize url', function (assert) { - map(assert, function (match) { - match('/').to('index'); - }); - - routes = { - post: createHandler('post'), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - let routeInfo = router.recognize('/'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'index'); - assert.equal(routeInfo.localName, 'index'); - assert.equal(routeInfo.parent, null); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, {}); - assert.deepEqual(routeInfo.params, {}); - assert.deepEqual(routeInfo.paramNames, []); - }); - - QUnit.test('top-level recognize url with params', function (assert) { - map(assert, function (match) { - match('/posts/:id').to('post'); - }); - - routes = { - post: createHandler('post'), - }; - - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - let routeInfo = router.recognize('/posts/123'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'post'); - assert.equal(routeInfo.localName, 'post'); - assert.equal(routeInfo.parent, null); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, {}); - assert.deepEqual(routeInfo.params, { id: '123' }); - assert.deepEqual(routeInfo.paramNames, ['id']); - }); - - QUnit.test('nested recognize url', function (assert) { - routes = { - postIndex: createHandler('postIndex'), - showPopularPosts: createHandler('showPopularPosts'), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - let routeInfo = router.recognize('/posts/popular'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'showPopularPosts'); - assert.equal(routeInfo.localName, 'showPopularPosts'); - assert.equal(routeInfo.parent!.name, 'postIndex'); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, {}); - assert.deepEqual(routeInfo.params, {}); - assert.deepEqual(routeInfo.paramNames, []); - }); - - QUnit.test('nested recognize url with params', function (assert) { - routes = { - postIndex: createHandler('postIndex'), - showFilteredPosts: createHandler('showFilteredPosts'), - }; - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - let routeInfo = router.recognize('/posts/filter/123'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'showFilteredPosts'); - assert.equal(routeInfo.localName, 'showFilteredPosts'); - assert.equal(routeInfo.parent!.name, 'postIndex'); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, {}); - assert.deepEqual(routeInfo.params, { filter_id: '123' }); - assert.deepEqual(routeInfo.paramNames, ['filter_id']); - }); - - QUnit.test('top-level recognize url with QPs', function (assert) { - map(assert, function (match) { - match('/').to('index'); - }); - - routes = { - index: createHandler('index'), - }; - - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - let routeInfo = router.recognize('/?a=123'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'index'); - assert.equal(routeInfo.localName, 'index'); - assert.equal(routeInfo.parent, null); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, { a: '123' }); - assert.deepEqual(routeInfo.params, {}); - assert.deepEqual(routeInfo.paramNames, []); - }); - - QUnit.test('nested recognize url with QPs', function (assert) { - routes = { - postIndex: createHandler('postIndex'), - showPopularPosts: createHandler('showPopularPosts'), - }; - - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - let routeInfo = router.recognize('/posts/popular?fizz=bar'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'showPopularPosts'); - assert.equal(routeInfo.localName, 'showPopularPosts'); - assert.equal(routeInfo.parent!.name, 'postIndex'); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, { fizz: 'bar' }); - assert.deepEqual(routeInfo.params, {}); - assert.deepEqual(routeInfo.paramNames, []); - }); - - QUnit.test('nested recognize url with QPs and params', function (assert) { - routes = { - postIndex: createHandler('postIndex'), - showFilteredPosts: createHandler('showFilteredPosts'), - }; - - assert.notOk(router.activeTransition, 'Does not start with an active transition'); - - let routeInfo = router.recognize('/posts/filter/123?fizz=bar'); - - assert.notOk(router.activeTransition, 'Does not create an active transition'); - - if (routeInfo === null) { - assert.ok(false); - return; - } - - router.replaceURL = () => { - assert.ok(false, 'Should not replace the URL'); - }; - - router.updateURL = () => { - assert.ok(false, 'Should not update the URL'); - }; - - assert.equal(routeInfo.name, 'showFilteredPosts'); - assert.equal(routeInfo.localName, 'showFilteredPosts'); - assert.equal(routeInfo.parent!.name, 'postIndex'); - assert.equal(routeInfo.child, null); - assert.deepEqual(routeInfo.queryParams, { fizz: 'bar' }); - assert.deepEqual(routeInfo.params, { filter_id: '123' }); - assert.deepEqual(routeInfo.paramNames, ['filter_id']); - }); - - QUnit.test('unrecognized url returns null', function (assert) { - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post'); - }); - - routes = { - post: createHandler('post'), - }; - let routeInfo = router.recognize('/fixzzz'); - assert.equal(routeInfo, null, 'Unrecognized url'); - }); - - QUnit.test('basic route change events with nested params', function (assert) { - assert.expect(14); - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id/foo').to('post'); - }); - - let enteredWillChange = 0; - let enteredDidChange = 0; - routes = { - post: createHandler('post'), - }; - - let newParam = false; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (newParam) { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'post'); - assert.deepEqual(transition.to!.params, { id: '2' }); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.params, { id: '1' }); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (newParam) { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'post'); - assert.deepEqual(transition.to!.params, { id: '2' }); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.params, { id: '1' }); - } - }; - - router - .transitionTo('/posts/1/foo') - .then(() => { - newParam = true; - return router.transitionTo('/posts/2/foo'); - }) - .then(() => { - assert.equal(enteredWillChange, 2); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('basic route change events with query params', function (assert) { - assert.expect(20); - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post'); - }); - - let enteredWillChange = 0; - let enteredDidChange = 0; - routes = { - post: createHandler('post'), - }; - - let newParam = false; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (newParam) { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'post'); - assert.deepEqual(transition.to!.queryParams, { trk: 'b' }); - assert.deepEqual(isPresent(transition.from) && transition.from!.queryParams, { trk: 'a' }); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.queryParams, { trk: 'a' }); - } - - assert.deepEqual(transition.to!.params, { id: '1' }); - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (newParam) { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'post'); - assert.deepEqual(transition.to!.queryParams, { trk: 'b' }); - assert.deepEqual(isPresent(transition.from) && transition.from!.queryParams, { trk: 'a' }); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.queryParams, { trk: 'a' }); - } - - assert.deepEqual(transition.to!.params, { id: '1' }); - }; - - router - .transitionTo('/posts/1?trk=a') - .then(() => { - newParam = true; - return router.transitionTo('/posts/1?trk=b'); - }) - .then(() => { - assert.equal(enteredWillChange, 2); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('basic route to one with query params', function (assert) { - assert.expect(8); - map(assert, function (match) { - match('/').to('index'); - match('/search').to('search'); - }); - - routes = { - search: createHandler('search'), - }; - - let newParam = false; - - router.routeWillChange = (transition: Transition) => { - if (newParam) { - assert.deepEqual(transition.to!.queryParams, { term: 'b' }, 'going to page with qps'); - assert.deepEqual( - isPresent(transition.from) && transition.from!.queryParams, - {}, - 'from never has qps' - ); - } else { - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.queryParams, {}); - } - }; - - router.routeDidChange = (transition: Transition) => { - if (newParam) { - assert.deepEqual(transition.to!.queryParams, { term: 'b' }); - assert.deepEqual(isPresent(transition.from) && transition.from!.queryParams, {}); - } else { - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.queryParams, {}); - } - }; - - router.transitionTo('/').then(() => { - newParam = true; - return router.transitionTo('search', { queryParams: { term: 'b' } }); - }); - }); - - QUnit.test( - 'calling recognize should not affect the transition.from query params for subsequent transitions', - function (assert) { - assert.expect(12); - map(assert, function (match) { - match('/').to('index'); - match('/search').to('search'); - }); - - routes = { - search: createHandler('search'), - }; - - let firstParam = false; - let secondParam = false; - - router.routeWillChange = (transition: Transition) => { - if (secondParam) { - assert.deepEqual( - transition.to!.queryParams, - { term: 'c' }, - 'going to next page with qps' - ); - assert.deepEqual( - isPresent(transition.from) && transition.from!.queryParams, - { term: 'b' }, - 'has previous qps' - ); - } else if (firstParam) { - assert.deepEqual(transition.to!.queryParams, { term: 'b' }, 'going to page with qps'); - assert.deepEqual( - isPresent(transition.from) && transition.from!.queryParams, - {}, - 'from never has qps' - ); - } else { - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.queryParams, {}); - } - }; - - router.routeDidChange = (transition: Transition) => { - if (secondParam) { - assert.deepEqual(transition.to!.queryParams, { term: 'c' }); - assert.deepEqual(isPresent(transition.from) && transition.from!.queryParams, { - term: 'b', - }); - } else if (firstParam) { - assert.deepEqual(transition.to!.queryParams, { term: 'b' }); - assert.deepEqual(isPresent(transition.from) && transition.from!.queryParams, {}); - } else { - assert.equal(transition.from, null); - assert.deepEqual(transition.to!.queryParams, {}); - } - }; - - router - .transitionTo('/') - .then(() => { - firstParam = true; - return router.transitionTo('search', { queryParams: { term: 'b' } }); - }) - .then(() => { - secondParam = true; - router.recognize('/search?wat=foo'); - return router.transitionTo({ queryParams: { term: 'c' } }); - }); - } - ); - - QUnit.test('redirects route events', function (assert) { - assert.expect(19); - map(assert, function (match) { - match('/').to('index'); - match('/posts', function (match) { - match('/:id').to('post'); - match('/details').to('postDetails'); - }); - match('/foo', function (match) { - match('/').to('foo', function (match) { - match('/bar').to('bar'); - }); - }); - match('/ok').to('ok'); - }); - - let redirected1 = false; - let redirected2 = false; - let initial = true; - let enteredWillChange = 0; - let enteredDidChange = 0; - - routes = { - post: createHandler('post', { - model() { - redirected1 = true; - router.transitionTo('/foo/bar'); - }, - }), - foo: createHandler('foo', { - model() { - redirected1 = false; - redirected2 = true; - router.transitionTo('/ok'); - }, - }), - ok: createHandler('ok'), - bar: createHandler('bar'), - postDetails: createHandler('postDetails'), - }; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index'); - assert.equal(transition.from!, null); - assert.equal(transition.to!.parent, null); - } else if (redirected1) { - assert.equal(transition.to!.localName, 'bar'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - assert.equal(transition.to!.parent!.localName, 'foo'); - } else if (redirected2) { - assert.equal(transition.to!.localName, 'ok'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - assert.equal(transition.to!.parent!, null); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - assert.equal(transition.to!.parent, null); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index'); - assert.equal(transition.from!, null); - initial = false; - } else { - assert.equal(transition.to!.localName, 'ok'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - } - }; - - router - .transitionTo('/') - .then(() => { - return router.transitionTo('/posts/1'); - }) - .catch((err: any) => { - console.log(err); - assert.equal(err.name, 'TransitionAborted'); - return router.activeTransition as any; - }) - .then(() => { - assert.equal(enteredWillChange, 4); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('abort route events', function (assert) { - assert.expect(19); - map(assert, function (match) { - match('/').to('index'); - match('/posts', function (match) { - match('/:id').to('post'); - match('/details').to('postDetails'); - }); - match('/foo', function (match) { - match('/').to('foo', function (match) { - match('/bar').to('bar'); - }); - }); - }); - - let redirected = false; - let initial = true; - let aborted = false; - let enteredWillChange = 0; - let enteredDidChange = 0; - - routes = { - post: createHandler('post', { - model() { - redirected = true; - router.transitionTo('/foo/bar'); - }, - }), - foo: createHandler('foo', { - model(_model: any, transition: Transition) { - aborted = true; - redirected = false; - transition.abort(); - }, - }), - bar: createHandler('bar'), - postDetails: createHandler('postDetails'), - }; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index'); - assert.equal(transition.from!, null); - assert.equal(transition.to!.parent, null); - } else if (redirected) { - assert.equal(transition.to!.localName, 'bar'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - assert.equal(transition.to!.parent!.localName, 'foo'); - } else if (aborted) { - assert.true(transition.isAborted); - assert.equal(transition.to, transition.from); - assert.equal(transition.to!.localName, 'index'); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - assert.equal(transition.to!.parent, null); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index'); - assert.equal(transition.from!, null); - initial = false; - } else { - assert.equal(transition.to!.localName, 'index'); - assert.equal(isPresent(transition.from) && transition.from.localName, 'index'); - } - }; - - router - .transitionTo('/') - .then(() => { - return router.transitionTo('/posts/1'); - }) - .catch((err: any) => { - assert.equal(err.name, 'TransitionAborted'); - return router.activeTransition as any; - }) - .then(() => { - assert.equal(enteredWillChange, 4); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('abort query param only', function (assert) { - assert.expect(6); - map(assert, function (match) { - match('/').to('index'); - }); - - routes = { - search: createHandler('search'), - }; - - let newParam = false; - let initial = true; - - router.updateURL = function (updateUrl) { - url = updateUrl; - if (!initial) { - assert.ok(false, 'updateURL should not be called'); - } - }; - - router.routeWillChange = (transition: Transition) => { - if (!transition.isAborted) { - if (newParam) { - assert.deepEqual(transition.to!.queryParams, { term: 'b' }, 'going to page with qps'); - assert.deepEqual( - isPresent(transition.from) && transition.from!.queryParams, - {}, - 'from never has qps' - ); - } else { - assert.strictEqual(transition.from, null, 'null transition from'); - assert.deepEqual(transition.to!.queryParams, {}, 'empty transition queryParams'); - } - } - if (!initial) { - if (!transition.isAborted) { - newParam = false; - transition.abort(); - } - } - }; - - router.routeDidChange = (transition: Transition) => { - if (!transition.isAborted) { - assert.strictEqual(transition.from, null, 'routeDidChange null from transition'); - assert.deepEqual(transition.to!.queryParams, {}, 'routeDidChange empty queryParams'); - } - }; - - router.transitionTo('/').then(() => { - newParam = true; - initial = false; - return router.transitionTo({ queryParams: { term: 'b' } }); - }); - }); - - QUnit.test('always has a transition through the substates', function (assert) { - map(assert, function (match) { - match('/').to('index'); - match('/posts', function (match) { - match('/:id').to('post'); - match('/details').to('postDetails'); - }); - match('/foo', function (match) { - match('/').to('foo', function (match) { - match('/bar').to('bar'); - }); - }); - match('/err').to('fooError'); - }); - - let enterSubstate = false; - let initial = true; - let isAborted = false; - let errorHandled = false; - let enteredWillChange = 0; - let enteredDidChange = 0; - - routes = { - post: createHandler('post', { - beforeModel(transition: Transition) { - isAborted = true; - transition.abort(); - enterSubstate = true; - router.intermediateTransitionTo('fooError'); - }, - }), - foo: createHandler('foo'), - }; - - router.transitionDidError = (error: TransitionError, transition: Transition) => { - if (error.wasAborted || transition.isAborted) { - return logAbort(transition); - } else { - transition.trigger(false, 'error', error.error, transition, error.route); - if (errorHandled) { - transition.rollback(); - router.routeDidChange(transition); - return transition; - } else { - transition.abort(); - return error.error; - } - } - }; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index', 'initial'); - assert.equal(transition.from!, null, 'initial'); - assert.equal(transition.to!.parent, null, 'initial'); - } else if (isAborted) { - assert.equal(transition.to!.localName, 'index', 'aborted'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index', 'aborted'); - } else if (enterSubstate) { - assert.equal(transition.to!.localName, 'fooError', 'substate'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index', 'substate'); - if (transition.to?.parent) { - assert.equal(transition.to.parent.localName, 'foo', 'substate'); - } - } else { - assert.equal(transition.to!.localName, 'post', 'to post'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index', 'to post'); - assert.equal(transition.to!.parent, null, 'to post'); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index', 'initial'); - assert.equal(transition.from!, null, 'initial'); - initial = false; - } else if (isAborted) { - assert.equal(transition.to!.localName, 'index', 'aborted'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index', 'aborted'); - isAborted = false; - } else { - assert.equal(transition.to!.localName, 'bar'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index'); - } - }; - - router - .transitionTo('/') - .then(() => { - return router.transitionTo('/posts/1'); - }) - .catch((err: any) => { - assert.equal(err.name, 'TransitionAborted'); - return router.activeTransition as any; - }) - .finally(() => { - assert.equal(enteredWillChange, 4); - assert.equal(enteredDidChange, 2); - }); - }); - - QUnit.test('error route events', function (assert) { - map(assert, function (match) { - match('/').to('index'); - match('/posts', function (match) { - match('/:id').to('post'); - match('/details').to('postDetails'); - }); - match('/foo', function (match) { - match('/').to('foo', function (match) { - match('/bar').to('bar'); - }); - }); - match('/err').to('fooError'); - }); - - let redirected = false; - let initial = true; - let errored = false; - let errorHandled = false; - let enteredWillChange = 0; - let enteredDidChange = 0; - - routes = { - post: createHandler('post', { - model() { - redirected = true; - router.transitionTo('/foo/bar'); - }, - }), - foo: createHandler('foo', { - model() { - errored = true; - redirected = false; - throw new Error('boom'); - }, - events: { - error() { - errorHandled = true; - router.intermediateTransitionTo('fooError'); - }, - }, - }), - fooError: createHandler('fooError'), - bar: createHandler('bar'), - postDetails: createHandler('postDetails'), - }; - - router.transitionDidError = (error: TransitionError, transition: Transition) => { - if (error.wasAborted || transition.isAborted) { - return logAbort(transition); - } else { - transition.trigger(false, 'error', error.error, transition, error.route); - if (errorHandled) { - transition.rollback(); - router.toInfos(transition, router.state!.routeInfos, true); - router.routeDidChange(transition); - return transition; - } else { - transition.abort(); - return error.error; - } - } - }; - - router.routeWillChange = (transition: Transition) => { - enteredWillChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index'); - assert.equal(transition.from!, null); - assert.equal(transition.to!.parent, null); - } else if (redirected) { - assert.equal(transition.to!.localName, 'bar'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index'); - assert.equal(transition.to!.parent!.localName, 'foo'); - } else if (errored) { - assert.false(transition.isAborted); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index'); - assert.equal(transition.to!.localName, 'fooError'); - } else { - assert.equal(transition.to!.localName, 'post'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index'); - assert.equal(transition.to!.parent, null); - } - }; - - router.routeDidChange = (transition: Transition) => { - enteredDidChange++; - if (initial) { - assert.equal(transition.to!.localName, 'index'); - assert.equal(transition.from!, null); - initial = false; - } else { - assert.equal(transition.to!.localName, 'fooError'); - assert.equal(isPresent(transition.from) && transition.from!.localName, 'index'); - } - }; - - const transition = router - .transitionTo('/') - .then(() => { - return router.transitionTo('/posts/1'); - }) - .catch((err: any) => { - assert.equal(err.name, 'TransitionAborted'); - return router.activeTransition as any; - }) - .finally(() => { - assert.equal(enteredWillChange, 4); - assert.equal(enteredDidChange, 2); - }); - - assert.rejects(transition); - }); - - QUnit.test( - "when transitioning to a new parent and child state, the parent's context should be available to the child's model", - function (assert) { - type Post = IModel; - - assert.expect(1); - let contexts: Array = []; - - map(assert, function (match) { - match('/').to('index'); - match('/posts/:id').to('post', function (match) { - match('/details').to('postDetails'); - }); - }); - - routes = { - post: createHandler('post', { - model: function () { - return contexts; - }, - }), - - postDetails: createHandler('postDetails', { - name: 'postDetails', - afterModel: function (_model: Post, transition: Transition) { - contexts.push(transition.resolvedModels['post'] as Post | undefined); - }, - }), - }; - - router - .handleURL('/') - .then(function () { - // This is a crucial part of the test - // In some cases, calling `generate` was preventing `model` from being called - router.generate('postDetails', { id: 1 }); - - return router.transitionTo('postDetails', { id: 1 }); - }, shouldNotHappen(assert)) - .then(function (value) { - assert.deepEqual(contexts, [{ id: 1 }], 'parent context is available'); - return value; - }, shouldNotHappen(assert)); - } - ); - - QUnit.test('handleURL: Handling a nested URL triggers each handler', function (assert) { - assert.expect(37); - - let posts: Dict[] = []; - let allPosts = { all: true }; - let popularPosts = { popular: true }; - let amazingPosts = { id: 'amazing' }; - let sadPosts = { id: 'sad' }; - - let counter = 0; - - let postIndexHandler = createHandler('postIndex', { - model: function (params: Dict, transition: Transition) { - assert.equal(transition.from, null, 'initial transition'); - assert.equal(transition.to && transition.to.localName, 'showAllPosts', 'going to leaf'); - // this will always get called, since it's at the root - // of all of the routes tested here - assert.deepEqual( - params, - { queryParams: {} }, - 'params should be empty in postIndexHandler#model' - ); - return posts; - }, - - setup: function (context: unknown) { - if (counter === 0) { - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be set up in postIndexHandler#setup' - ); - assert.strictEqual( - context, - posts, - 'The object passed in to postIndexHandler#setup should be posts' - ); - } else { - assert.ok(false, 'Should not get here'); - } - }, - }); - - let showAllPostsHandler = createHandler('showAllPosts', { - model(params: Dict, transition: Transition) { - if (counter > 0 && counter < 4) { - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be set up in showAllPostsHandler#model' - ); - } - - if (counter < 4) { - assert.equal(transition.from, null, 'initial transition'); - assert.equal(transition.to && transition.to.localName, 'showAllPosts', 'at leaf'); - assert.deepEqual( - params, - { queryParams: {} }, - 'params should be empty in showAllPostsHandler#model' - ); - return allPosts; - } else { - assert.ok(false, 'Should not get here'); - } - - return; - }, - - setup(context: Dict) { - if (counter === 0) { - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be set up in showAllPostsHandler#setup' - ); - assert.equal( - showAllPostsHandler.context, - allPosts, - 'showAllPostsHandler context should be set up in showAllPostsHandler#setup' - ); - assert.strictEqual( - context, - allPosts, - 'The object passed in should be allPosts in showAllPostsHandler#setup' - ); - } else { - assert.ok(false, 'Should not get here'); - } - }, - }); - - let showPopularPostsHandler = createHandler('showPopularPosts', { - model: function (params: Dict) { - if (counter < 3) { - assert.ok(false, 'Should not get here'); - } else if (counter === 3) { - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be set up in showPopularPostsHandler#model' - ); - assert.deepEqual( - params, - { queryParams: {} }, - 'params should be empty in showPopularPostsHandler#serialize' - ); - return popularPosts; - } else { - assert.ok(false, 'Should not get here'); - } - - return; - }, - - setup: function (context: Dict) { - if (counter === 3) { - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be set up in showPopularPostsHandler#setup' - ); - assert.equal( - showPopularPostsHandler.context, - popularPosts, - 'showPopularPostsHandler context should be set up in showPopularPostsHandler#setup' - ); - assert.strictEqual( - context, - popularPosts, - 'The object passed to showPopularPostsHandler#setup should be popular posts' - ); - } else { - assert.ok(false, 'Should not get here'); - } - }, - }); - - let showFilteredPostsHandler = createHandler('showFilteredPosts', { - model: function (params: Dict, transition: Transition) { - if (counter < 4) { - assert.ok(false, 'Should not get here'); - } else if (counter === 4) { - assert.equal(transition.from && transition.from.localName, 'showPopularPosts'); - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be set up in showFilteredPostsHandler#model' - ); - assert.deepEqual( - params, - { filter_id: 'amazing', queryParams: {} }, - "params should be { filter_id: 'amazing' } in showFilteredPostsHandler#model" - ); - return amazingPosts; - } else if (counter === 5) { - assert.equal( - transition.from && transition.from.localName, - 'showFilteredPosts', - 'came from same route' - ); - assert.equal( - transition.to && transition.to.localName, - 'showFilteredPosts', - 'going to same route' - ); - assert.equal(transition.from?.params?.['filter_id'], 'amazing', 'old params'); - assert.equal(transition.to?.params?.['filter_id'], 'sad', 'new params'); - assert.equal( - postIndexHandler.context, - posts, - 'postIndexHandler context should be posts in showFilteredPostsHandler#model' - ); - assert.deepEqual( - params, - { filter_id: 'sad', queryParams: {} }, - "params should be { filter_id: 'sad' } in showFilteredPostsHandler#model" - ); - return sadPosts; - } else { - assert.ok(false, 'Should not get here'); - } - - return; - }, - - setup: function (context: Dict) { - if (counter === 4) { - assert.equal(postIndexHandler.context, posts); - assert.equal(showFilteredPostsHandler.context, amazingPosts); - assert.strictEqual(context, amazingPosts); - } else if (counter === 5) { - assert.equal(postIndexHandler.context, posts); - assert.equal(showFilteredPostsHandler.context, sadPosts); - assert.strictEqual(context, sadPosts); - } else { - assert.ok(false, 'Should not get here'); - } - }, - }); - - routes = { - postIndex: postIndexHandler, - showAllPosts: showAllPostsHandler, - showPopularPosts: showPopularPostsHandler, - showFilteredPosts: showFilteredPostsHandler, - }; - - router - .transitionTo('/posts') - .then(function () { - assert.ok(true, '1: Finished, trying /posts/all'); - counter++; - return router.transitionTo('/posts/all'); - }, shouldNotHappen(assert)) - .then(function () { - assert.ok(true, '2: Finished, trying /posts'); - counter++; - return router.transitionTo('/posts'); - }, shouldNotHappen(assert)) - .then(function () { - assert.ok(true, '3: Finished, trying /posts/popular'); - counter++; - return router.transitionTo('/posts/popular'); - }, shouldNotHappen(assert)) - .then(function () { - assert.ok(true, '4: Finished, trying /posts/filter/amazing'); - counter++; - return router.transitionTo('/posts/filter/amazing'); - }, shouldNotHappen(assert)) - .then(function () { - assert.ok(true, '5: Finished, trying /posts/filter/sad'); - counter++; - return router.transitionTo('/posts/filter/sad'); - }, shouldNotHappen(assert)) - .then(function () { - assert.ok(true, '6: Finished!'); - }, shouldNotHappen(assert)); - }); - - QUnit.test('it can handle direct transitions to named routes', function (assert) { - let allPosts = { all: true }; - let popularPosts = { popular: true }; - let amazingPosts = { filter: 'amazing' }; - let sadPosts = { filter: 'sad' }; - - let postIndexHandler = createHandler('postIndex', { - model: function () { - return allPosts; - }, - - serialize: function () { - return {}; - }, - }); - - let showAllPostsHandler = createHandler('showAllPosts', { - model: function () { - //assert.ok(!params, 'params is falsy for non dynamic routes'); - return allPosts; - }, - - serialize: function () { - return {}; - }, - - setup: function (context: Dict) { - assert.strictEqual(context, allPosts, 'showAllPosts should get correct setup'); - }, - }); - - let showPopularPostsHandler = createHandler('showPopularPosts', { - model: function () { - return popularPosts; - }, - - serialize: function () { - return {}; - }, - - setup: function (context: Dict) { - assert.strictEqual( - context, - popularPosts, - 'showPopularPosts#setup should be called with the deserialized value' - ); - }, - }); - - let showFilteredPostsHandler = createHandler('showFilteredPosts', { - model: function (params: Dict) { - if (!params) { - return; - } - if (params['filter_id'] === 'amazing') { - return amazingPosts; - } else if (params['filter_id'] === 'sad') { - return sadPosts; - } - return; - }, - - serialize: function (context: Dict, params: string[]) { - assert.deepEqual(params, ['filter_id'], 'showFilteredPosts should get correct serialize'); - return { filter_id: context['filter'] }; - }, - - setup: function (context: Dict) { - if (counter === 2) { - assert.strictEqual( - context, - amazingPosts, - 'showFilteredPosts should get setup with amazingPosts' - ); - } else if (counter === 3) { - assert.strictEqual( - context, - sadPosts, - 'showFilteredPosts should get setup setup with sadPosts' - ); - } - }, - }); - - routes = { - postIndex: postIndexHandler, - showAllPosts: showAllPostsHandler, - showPopularPosts: showPopularPostsHandler, - showFilteredPosts: showFilteredPostsHandler, - }; - - router.updateURL = function (url) { - let expected: Dict = { - 0: '/posts', - 1: '/posts/popular', - 2: '/posts/filter/amazing', - 3: '/posts/filter/sad', - 4: '/posts', - }; - - assert.equal(url, expected[counter], 'updateURL should be called with correct url'); - }; - - let counter = 0; - - router - .handleURL('/posts') - .then(function () { - return router.transitionTo('showAllPosts'); - }, shouldNotHappen(assert)) - .then(function () { - counter++; - return router.transitionTo('showPopularPosts'); - }, shouldNotHappen(assert)) - .then(function () { - counter++; - return router.transitionTo('showFilteredPosts', amazingPosts); - }, shouldNotHappen(assert)) - .then(function () { - counter++; - return router.transitionTo('showFilteredPosts', sadPosts); - }, shouldNotHappen(assert)) - .then(function () { - counter++; - return router.transitionTo('showAllPosts'); - }, shouldNotHappen(assert)); - }); - - QUnit.test('replaceWith calls replaceURL', function (assert) { - let updateCount = 0, - replaceCount = 0; - - router.updateURL = function () { - updateCount++; - }; - - router.replaceURL = function () { - replaceCount++; - }; - - router - .handleURL('/posts') - .then(function () { - return router.replaceWith('about'); - }) - .then(function () { - assert.equal(updateCount, 0, 'should not call updateURL'); - assert.equal(replaceCount, 1, 'should call replaceURL once'); - }); - }); - - QUnit.test( - 'applyIntent returns a tentative state based on a named transition', - async function (assert) { - await router.transitionTo('/posts'); - let state = router.applyIntent('faq', []); - assert.ok(state.routeInfos.length); - } - ); - - QUnit.test('Moving to a new top-level route triggers exit callbacks', function (assert) { - assert.expect(6); - - let allPosts = { posts: 'all' }; - let postsStore: Dict = { 1: { id: 1 }, 2: { id: 2 } }; - let currentId: number; - let currentPath: string; - - routes = { - showAllPosts: createHandler('showAllPosts', { - model: function () { - return allPosts; - }, - - setup: function (posts: Dict, transition: Transition) { - assert.notOk(isExiting(this as unknown as Route, transition.routeInfos)); - assert.equal( - posts, - allPosts, - 'The correct context was passed into showAllPostsHandler#setup' - ); - currentPath = 'postIndex.showAllPosts'; - }, - - exit: function (transition: Transition) { - assert.ok(isExiting(this as unknown as Route, transition.routeInfos)); - }, - }), - - showPost: createHandler('showPost', { - model: function (params: Dict) { - let id = parseInt(params['id'] as string, 10); - return postsStore[id]; - }, - - serialize: function (post: Dict) { - return { id: post['id'] }; - }, - - setup: function (post: Dict) { - currentPath = 'showPost'; - assert.equal(post['id'], currentId, 'The post id is ' + currentId); - }, - }), - }; - - router - .handleURL('/posts') - .then(function () { - expectedUrl = '/posts/1'; - currentId = 1; - return router.transitionTo('showPost', postsStore[1]); - }, shouldNotHappen(assert)) - .then(function () { - assert.equal(routePath(router.currentRouteInfos!), currentPath); - }, shouldNotHappen(assert)); - }); - - QUnit.test('pivotHandler is exposed on Transition object', function (assert) { - assert.expect(3); - - routes = { - showAllPosts: createHandler('showAllPosts', { - beforeModel: function (transition: Transition) { - assert.notOk(transition.pivotHandler, 'First route transition has no pivot route'); - }, - }), - - showPopularPosts: createHandler('showPopularPosts', { - beforeModel: function (transition: Transition) { - assert.equal( - transition.pivotHandler, - routes['postIndex'], - 'showAllPosts -> showPopularPosts pivotHandler is postIndex' - ); - }, - }), - - postIndex: createHandler('postIndex'), - - about: createHandler('about', { - beforeModel: function (transition: Transition) { - assert.notOk(transition.pivotHandler, 'top-level transition has no pivotHandler'); - }, - }), - }; - - router - .handleURL('/posts') - .then(function () { - return router.transitionTo('showPopularPosts'); - }) - .then(function () { - return router.transitionTo('about'); - }); - }); - - QUnit.test('transition.resolvedModels after redirects b/w routes', async function (assert) { - type Application = { app: boolean } & IModel; - - assert.expect(3); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/peter').to('peter'); - match('/wagenet').to('wagenet'); - }); - }); - - let app = { app: true }; - - routes = { - application: createHandler('application', { - model: function () { - assert.ok(true, 'application#model'); - return app; - }, - }), - - peter: createHandler('peter', { - model: function (_params: Dict, transition: Transition) { - assert.deepEqual( - transition.resolvedModels['application'] as Application, - app, - 'peter: resolvedModel correctly stored in resolvedModels for parent route' - ); - router.transitionTo('wagenet'); - }, - }), - wagenet: createHandler('wagenet', { - model: function (_params: Dict, transition: Transition) { - assert.deepEqual( - transition.resolvedModels['application'] as Application | undefined, - app, - 'wagenet: resolvedModel correctly stored in resolvedModels for parent route' - ); - }, - }), - }; - - return ignoreTransitionError(router.transitionTo('/peter')); - }); - - QUnit.test( - 'transition.resolvedModels after redirects within the same route', - async function (assert) { - type Admin = IModel & { admin: boolean }; - - let admin = { admin: true }, - redirect = true; - - routes = { - admin: createHandler('admin', { - model: function () { - assert.ok(true, 'admin#model'); - return admin; - }, - }), - - adminPosts: createHandler('adminPosts', { - model: function (_params: Dict, transition: Transition) { - assert.deepEqual( - transition.resolvedModels['admin'] as Admin | undefined, - admin, - 'resolvedModel correctly stored in resolvedModels for parent route' - ); - if (redirect) { - redirect = false; - router.transitionTo('adminPosts'); - } - }, - }), - }; - - await router.transitionTo('/posts/admin/1/posts'); - } - ); - - QUnit.test( - `transition.to.find's callback is always called with defined routeInfo`, - async function (assert) { - type Application = { app: boolean } & IModel; - - assert.expect(3); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/peter').to('peter', function (match) { - match('/wagenet').to('wagenet'); - }); - }); - }); - - routes = { - application: createHandler('application'), - peter: createHandler('peter'), - wagenet: createHandler('wagenet', { - model: function (_params: Dict, transition: Transition) { - transition.to!.find((routeInfo) => { - assert.ok(routeInfo, 'routeInfo is defined'); - return false; - }); - }, - }), - }; - - await router.transitionTo('/peter/wagenet'); - } - ); - - QUnit.test( - 'Moving to the same route with a different parent dynamic segment re-runs model', - async function (assert) { - let admins: Dict = { 1: { id: 1 }, 2: { id: 2 } }, - adminPosts: Dict = { 1: { id: 1 }, 2: { id: 2 } }; - - routes = { - admin: createHandler('admin', { - currentModel: -1, - model: function (params: Dict) { - return (this['currentModel'] = admins[params['id'] as string]); - }, - }), - - adminPosts: createHandler('adminPosts', { - model: function () { - return adminPosts[(routes['admin'] as any).currentModel.id]; - }, - }), - }; - - await router.transitionTo('/posts/admin/1/posts'); - assert.equal(routes['admin']!.context, admins[1]); - assert.equal(routes['adminPosts']!.context, adminPosts[1]); - - await router.transitionTo('/posts/admin/2/posts'); - assert.equal(routes['admin']!.context, admins[2]); - assert.equal(routes['adminPosts']!.context, adminPosts[2]); - } - ); - - QUnit.test( - 'Moving to a sibling route only triggers exit callbacks on the current route (when transitioned internally)', - async function (assert) { - assert.expect(8); - - let allPosts = { posts: 'all' }; - - let showAllPostsHandler = createHandler('showAllPosts', { - model: function () { - return allPosts; - }, - - setup: function (posts: Dict) { - assert.equal( - posts, - allPosts, - 'The correct context was passed into showAllPostsHandler#setup' - ); - }, - - enter: function () { - assert.ok(true, 'The sibling handler should be entered'); - }, - - exit: function () { - assert.ok(true, 'The sibling handler should be exited'); - }, - }); - - let filters: Dict = {}; - - let showFilteredPostsHandler = createHandler('showFilteredPosts', { - enter: function () { - assert.ok(true, 'The new handler was entered'); - }, - - exit: function () { - assert.ok(false, 'The new handler should not be exited'); - }, - - model: function (params: Dict) { - let id = params['filter_id'] as string; - if (!filters[id]) { - filters[id] = { id: id }; - } - - return filters[id]; - }, - - serialize: function (filter: Dict) { - assert.equal(filter['id'], 'favorite', "The filter should be 'favorite'"); - return { filter_id: filter['id'] }; - }, - - setup: function (filter: Dict) { - assert.equal( - filter['id'], - 'favorite', - 'showFilteredPostsHandler#setup was called with the favorite filter' - ); - }, - }); - - let postIndexHandler = createHandler('postIndex', { - enter: function () { - assert.ok(true, 'The outer handler was entered only once'); - }, - - exit: function () { - assert.ok(false, 'The outer handler was not exited'); - }, - }); - - routes = { - postIndex: postIndexHandler, - showAllPosts: showAllPostsHandler, - showFilteredPosts: showFilteredPostsHandler, - }; - - await router.handleURL('/posts').then(function () { - expectedUrl = '/posts/filter/favorite'; - return router.transitionTo('showFilteredPosts', { id: 'favorite' }); - }); - } - ); - - QUnit.test( - 'Moving to a sibling route only triggers exit callbacks on the current route (when transitioned via a URL change)', - async function (assert) { - assert.expect(7); - - let allPosts = { posts: 'all' }; - - let showAllPostsHandler = createHandler('showAllPostsHandler', { - model: function () { - return allPosts; - }, - - setup: function (posts: Dict) { - assert.equal( - posts, - allPosts, - 'The correct context was passed into showAllPostsHandler#setup' - ); - }, - - enter: function () { - assert.ok(true, 'The sibling handler should be entered'); - }, - - exit: function () { - assert.ok(true, 'The sibling handler should be exited'); - }, - }); - - let filters: Dict = {}; - - let showFilteredPostsHandler = createHandler('showFilteredPosts', { - enter: function () { - assert.ok(true, 'The new handler was entered'); - }, - - exit: function () { - assert.ok(false, 'The new handler should not be exited'); - }, - - model: function (params: Dict) { - assert.equal(params['filter_id'], 'favorite', "The filter should be 'favorite'"); - - let id = params['filter_id'] as string; - if (!filters[id]) { - filters[id] = { id: id }; - } - - return filters[id]; - }, - - serialize: function (filter: Dict) { - return { filter_id: filter['id'] }; - }, - - setup: function (filter: Dict) { - assert.equal( - filter['id'], - 'favorite', - 'showFilteredPostsHandler#setup was called with the favorite filter' - ); - }, - }); - - let postIndexHandler = createHandler('postIndex', { - enter: function () { - assert.ok(true, 'The outer handler was entered only once'); - }, - - exit: function () { - assert.ok(false, 'The outer handler was not exited'); - }, - }); - - routes = { - postIndex: postIndexHandler, - showAllPosts: showAllPostsHandler, - showFilteredPosts: showFilteredPostsHandler, - }; - - await router.handleURL('/posts'); - - expectedUrl = '/posts/filter/favorite'; - await router.handleURL(expectedUrl); - } - ); - - QUnit.test('events can be targeted at the current handler', async function (assert) { - assert.expect(2); - - routes = { - showPost: createHandler('showPost', { - enter: function () { - assert.ok(true, 'The show post handler was entered'); - }, - - events: { - expand: function () { - assert.equal(this, routes['showPost'], 'The handler is the `this` for the event'); - }, - }, - }), - }; - - await router.transitionTo('/posts/1'); - - router.trigger('expand'); - }); - - QUnit.test('event triggering is pluggable', function (assert) { - routes = { - showPost: createHandler('showPost', { - enter: function () { - assert.ok(true, 'The show post handler was entered'); - }, - - actions: { - expand: function () { - assert.equal(this, routes['showPost'], 'The handler is the `this` for the event'); - }, - }, - }), - }; - router.triggerEvent = function ( - handlerInfos: RouteInfo[], - ignoreFailure: boolean, - name: string, - args: any[] - ) { - if (!handlerInfos) { - if (ignoreFailure) { - return; - } - throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); - } - - for (let i = handlerInfos.length - 1; i >= 0; i--) { - let handlerInfo = handlerInfos[i], - handler = handlerInfo!.route as any; - - if (handler.actions && handler.actions[name]) { - if (handler.actions[name].apply(handler, args) !== true) { - return; - } - } - } - }; - router.handleURL('/posts/1').then(function () { - router.trigger('expand'); - }); - }); - - QUnit.test('Unhandled events raise an exception', function (assert) { - router.handleURL('/posts/1'); - - assert.throws(function () { - router.trigger('doesnotexist'); - }, /doesnotexist/); - }); - - QUnit.test('events can be targeted at a parent handler', async function (assert) { - assert.expect(3); - - routes = { - postIndex: createHandler('postIndex', { - enter: function () { - assert.ok(true, 'The post index handler was entered'); - }, - - events: { - expand: function () { - assert.equal(this, routes['postIndex'], 'The handler is the `this` in events'); - }, - }, - }), - showAllPosts: createHandler('showAllPosts', { - enter: function () { - assert.ok(true, 'The show all posts handler was entered'); - }, - }), - }; - - await router.transitionTo('/posts'); - router.trigger('expand'); - }); - - QUnit.test('events can bubble up to a parent handler via `return true`', function (assert) { - assert.expect(4); - - routes = { - postIndex: createHandler('postIndex', { - enter: function () { - assert.ok(true, 'The post index handler was entered'); - }, - - events: { - expand: function () { - assert.equal(this, routes['postIndex'], 'The handler is the `this` in events'); - }, - }, - }), - showAllPosts: createHandler('showAllPosts', { - enter: function () { - assert.ok(true, 'The show all posts handler was entered'); - }, - events: { - expand: function () { - assert.equal(this, routes['showAllPosts'], 'The handler is the `this` in events'); - return true; - }, - }, - }), - }; - - router.handleURL('/posts').then(function () { - router.trigger('expand'); - }); - }); - - QUnit.test( - "handled-then-bubbled events don't throw an exception if uncaught by parent route", - async function (assert) { - assert.expect(3); - - routes = { - postIndex: createHandler('postIndex', { - enter: function () { - assert.ok(true, 'The post index handler was entered'); - }, - }), - - showAllPosts: createHandler('showAllPosts', { - enter: function () { - assert.ok(true, 'The show all posts handler was entered'); - }, - events: { - expand: function () { - assert.equal(this, routes['showAllPosts'], 'The handler is the `this` in events'); - return true; - }, - }, - }), - }; - - await router.transitionTo('/posts'); - router.trigger('expand'); - } - ); - - QUnit.test('events only fire on the closest handler', function (assert) { - assert.expect(5); - - routes = { - postIndex: createHandler('postIndex', { - enter: function () { - assert.ok(true, 'The post index handler was entered'); - }, - - events: { - expand: function () { - assert.ok(false, 'Should not get to the parent handler'); - }, - }, - }), - - showAllPosts: createHandler('showAllPosts', { - enter: function () { - assert.ok(true, 'The show all posts handler was entered'); - }, - - events: { - expand: function (passedContext1: Dict, passedContext2: Dict) { - assert.equal(context1, passedContext1, 'A context is passed along'); - assert.equal(context2, passedContext2, 'A second context is passed along'); - assert.equal( - this, - routes['showAllPosts'], - 'The handler is passed into events as `this`' - ); - }, - }, - }), - }; - - let context1: Dict = {}, - context2: Dict = {}; - router.handleURL('/posts').then(function () { - router.trigger('expand', context1, context2); - }); - }); - - QUnit.test("Date params aren't treated as string/number params", function (assert) { - assert.expect(1); - - routes = { - showPostsForDate: createHandler('showPostsForDate', { - serialize: function (date: Date) { - return { - date: date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate(), - }; - }, - - model: function () { - assert.ok(false, "model shouldn't be called; the date is the provided model"); - }, - }), - }; - - if (scenario.async) { - serializers = { - showPostsForDate: function (date: any) { - return { - date: date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate(), - }; - }, - }; - } - - let result = router.generate('showPostsForDate', new Date(1815, 5, 18)); - assert.equal(result, '/posts/on/1815-5-18'); - }); - - QUnit.test('getSerializer takes precedence over handler.serialize', function (assert) { - assert.expect(2); - - router.getSerializer = function () { - return function (date: any) { - assert.ok(true, 'getSerializer called'); - return { - date: date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate(), - }; - }; - }; - - routes = { - showPostsForDate: createHandler('showPostsForDate', { - serialize: function () { - assert.ok(false, "serialize method shouldn't be called"); - return {}; - }, - - model: function () { - assert.ok(false, "model shouldn't be called; the date is the provided model"); - }, - }), - }; - - assert.equal(router.generate('showPostsForDate', new Date(1815, 5, 18)), '/posts/on/1815-5-18'); - }); - - QUnit.test('the serializer method is unbound', function (assert) { - assert.expect(1); - - router.getSerializer = function () { - return function (this: void, date: any) { - assert.equal(this, undefined); - return { - date: date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate(), - }; - }; - }; - - router.generate('showPostsForDate', new Date(1815, 5, 18)); - }); - - QUnit.test('params are known by a transition up front', async function (assert) { - assert.expect(2); - - routes = { - postIndex: createHandler('postIndex', { - model: function (_params: Dict, transition: Transition) { - assert.deepEqual(transition[PARAMS_SYMBOL], { - postIndex: {}, - showFilteredPosts: { filter_id: 'sad' }, - }); - }, - }), - showFilteredPosts: createHandler('showFilteredPosts', { - model: function (_params: Dict, transition: Transition) { - assert.deepEqual(transition[PARAMS_SYMBOL], { - postIndex: {}, - showFilteredPosts: { filter_id: 'sad' }, - }); - }, - }), - }; - - await router.transitionTo('/posts/filter/sad', 'blorg'); - }); - - QUnit.test( - 'transitionTo uses the current context if you are already in a handler with a context that is not changing', - async function (assert) { - let admin = { id: 47 }, - adminPost = { id: 74 }; - - routes = { - admin: createHandler('admin', { - serialize: function (object: Dict) { - assert.equal(object['id'], 47, 'The object passed to serialize is correct'); - return { id: 47 }; - }, - - model: function (params: Dict) { - assert.equal(params['id'], 47, 'The object passed to serialize is correct'); - return admin; - }, - }), - - adminPost: createHandler('adminPost', { - serialize: function (object: Dict) { - return { post_id: object['id'] }; - }, - - model: function (params: Dict) { - assert.equal(params['id'], 74, 'The object passed to serialize is correct'); - return adminPost; - }, - }), - }; - - expectedUrl = '/posts/admin/47/posts/74'; - await router.transitionTo('adminPost', admin, adminPost); - - expectedUrl = '/posts/admin/47/posts/75'; - await router.transitionTo('adminPost', { id: 75 }); - } - ); - - QUnit.test('check for mid-transition correctness', async function (assert) { - let posts: Dict = { - 1: { id: 1 }, - 2: { id: 2 }, - 3: { id: 3 }, - }; - - let showPostHandler = createHandler('showPost', { - serialize: function (object: Dict) { - return (object && { id: object['id'] }) || null; - }, - - model: function (params: Dict) { - let id = params['id'] as string; - return posts[id]; - }, - }); - - routes = { - showPost: showPostHandler, - }; - - // Get a reference to the transition, mid-transition. - router.willTransition = function () { - let midTransitionState = router.activeTransition![STATE_SYMBOL]; - - // Make sure that the activeIntent doesn't match post 300. - let isPost300Targeted = router.isActiveIntent( - 'showPost', - [300], - undefined, - midTransitionState - ); - assert.notOk(isPost300Targeted, 'Post 300 should not match post 3.'); - }; - - // Go to post 3. This triggers our test. - await router.transitionTo('/posts/3'); - }); - - QUnit.test( - 'tests whether arguments to transitionTo are considered active', - async function (assert) { - let admin = { id: 47 }, - adminPost = { id: 74 }, - posts: Dict = { - 1: { id: 1 }, - 2: { id: 2 }, - 3: { id: 3 }, - }; - - let adminHandler = createHandler('admin', { - serialize: function () { - return { id: 47 }; - }, - - model: function () { - return admin; - }, - }); - - let adminPostHandler = createHandler('adminPost', { - serialize: function (object: Dict) { - return { post_id: object['id'] }; - }, - - model: function () { - return adminPost; - }, - }); - - let showPostHandler = createHandler('showPost', { - serialize: function (object: Dict) { - return (object && { id: object['id'] }) || null; - }, - - model: function (params: Dict) { - return posts[params['id'] as string]; - }, - }); - - routes = { - admin: adminHandler, - adminPost: adminPostHandler, - showPost: showPostHandler, - }; - - await router.transitionTo('/posts/1'); - assert.ok(router.isActive('showPost'), 'The showPost handler is active'); - assert.ok( - router.isActive('showPost', posts[1]), - 'The showPost handler is active with the appropriate context' - ); - assert.notOk( - router.isActive('showPost', posts[2]), - 'The showPost handler is inactive when the context is different' - ); - assert.notOk(router.isActive('adminPost'), 'The adminPost handler is inactive'); - assert.notOk( - router.isActive('showPost', null), - 'The showPost handler is inactive with a null context' - ); - - await router.transitionTo('adminPost', admin, adminPost); - assert.ok(router.isActive('adminPost'), 'The adminPost handler is active'); - assert.ok( - router.isActive('adminPost', adminPost), - 'The adminPost handler is active with the current context' - ); - assert.ok( - router.isActive('adminPost', admin, adminPost), - 'The adminPost handler is active with the current and parent context' - ); - assert.ok(router.isActive('admin'), 'The admin handler is active'); - assert.ok(router.isActive('admin', admin), 'The admin handler is active with its context'); - } - ); - - QUnit.test( - 'calling generate on a non-dynamic route does not blow away parent contexts', - function (assert) { - map(assert, function (match) { - match('/projects').to('projects', function (match) { - match('/').to('projectsIndex'); - match('/project').to('project', function (match) { - match('/').to('projectIndex'); - }); - }); - }); - - let projects = {}; - - routes = { - projects: createHandler('projects', { - model: function () { - return projects; - }, - }), - }; - - router.handleURL('/projects').then(function () { - assert.equal(routes['projects']!.context, projects, 'projects handler has correct context'); - router.generate('projectIndex'); - assert.equal( - routes['projects']!.context, - projects, - 'projects handler retains correct context' - ); - }); - } - ); - - QUnit.test( - 'calling transitionTo on a dynamic parent route causes non-dynamic child context to be updated', - async function (assert) { - type Project = { project_id: string } & IModel; - - map(assert, function (match) { - match('/project/:project_id').to('project', function (match) { - match('/').to('projectIndex'); - }); - }); - - let projectHandler = createHandler('project', { - model: function (params: Dict) { - delete params['queryParams']; - return params; - }, - }); - - let projectIndexHandler = createHandler('projectIndex', { - model: function (_params: Dict, transition: Transition) { - return transition.resolvedModels['project']; - }, - }); - - routes = { - project: projectHandler, - projectIndex: projectIndexHandler, - }; - - await router.transitionTo('/project/1'); - assert.deepEqual( - projectHandler.context, - { project_id: '1' }, - 'project handler retains correct context' - ); - assert.deepEqual( - projectIndexHandler.context, - { project_id: '1' }, - 'project index handler has correct context' - ); - - router.generate('projectIndex', { project_id: '2' }); - - assert.deepEqual( - projectHandler.context, - { project_id: '1' }, - 'project handler retains correct context' - ); - assert.deepEqual( - projectIndexHandler.context, - { project_id: '1' }, - 'project index handler retains correct context' - ); - - await router.transitionTo('projectIndex', { project_id: '2' }); - assert.deepEqual( - projectHandler.context, - { project_id: '2' }, - 'project handler has updated context' - ); - assert.deepEqual( - projectIndexHandler.context, - { project_id: '2' }, - 'project index handler has updated context' - ); - } - ); - - QUnit.test( - 'reset exits and clears the current and target route handlers', - async function (assert) { - let postIndexExited = false; - let showAllPostsExited = false; - let steps = 0; - - assert.equal(++steps, 1); - let postIndexHandler = createHandler('postIndex', { - exit: function () { - postIndexExited = true; - assert.equal(++steps, 4); - }, - }); - let showAllPostsHandler = createHandler('showAllPosts', { - exit: function () { - showAllPostsExited = true; - assert.equal(++steps, 3); - }, - }); - routes = { - postIndex: postIndexHandler, - showAllPosts: showAllPostsHandler, - }; - - await router.transitionTo('/posts/all'); - - assert.equal(++steps, 2); - router.reset(); - - assert.ok(postIndexExited, 'Post index handler did not exit'); - assert.ok(showAllPostsExited, 'Show all posts handler did not exit'); - assert.equal(router.currentRouteInfos, null, 'currentHandlerInfos should be null'); - } - ); - - QUnit.test( - 'any of the model hooks can redirect with or without promise', - async function (assert) { - assert.expect(26); - let setupShouldBeEntered = false; - let returnPromise = false; - let redirectTo: string; - - function redirectToAbout() { - if (returnPromise) { - return reject().then(null, function () { - router.transitionTo(redirectTo); - }); - } else { - router.transitionTo(redirectTo); - } - return; - } - - routes = { - index: createHandler('index', { - beforeModel: redirectToAbout, - model: redirectToAbout, - afterModel: redirectToAbout, - - setup: function () { - assert.ok(setupShouldBeEntered, 'setup should be entered at this time'); - }, - }), - - about: createHandler('about', { - setup: function () { - assert.ok(true, "about handler's setup function was called"); - }, - }), - - borf: createHandler('borf', { - setup: function () { - assert.ok(true, 'borf setup entered'); - }, - }), - }; - - async function testStartup(assert: Assert, firstExpectedURL?: string) { - map(assert, function (match) { - match('/').to('index'); - match('/about').to('about'); - match('/foo').to('foo'); - match('/borf').to('borf'); - }); - - redirectTo = 'about'; - - // Perform a redirect on startup. - expectedUrl = firstExpectedURL || '/about'; - await ignoreTransitionError(router.transitionTo('/')); - - expectedUrl = '/borf'; - redirectTo = 'borf'; - - await ignoreTransitionError(router.transitionTo('index')); - } - - await testStartup(assert); - - returnPromise = true; - await testStartup(assert); - - delete routes['index']!.beforeModel; - returnPromise = false; - await testStartup(assert); - - returnPromise = true; - await testStartup(assert); - - delete routes['index']!.model; - returnPromise = false; - await testStartup(assert); - - returnPromise = true; - await testStartup(assert); - - delete routes['index']!.afterModel; - setupShouldBeEntered = true; - await testStartup(assert, '/'); - } - ); - - QUnit.test( - 'transitionTo with a promise pauses the transition until resolve, passes resolved context to setup', - async function (assert) { - routes = { - index: createHandler('index'), - showPost: createHandler('showPost', { - setup: function (context: Dict) { - assert.deepEqual(context, { id: 1 }, 'setup receives a resolved context'); - }, - }), - }; - - await router.transitionTo('/index'); - - await router.transitionTo( - 'showPost', - new Promise(function (resolve) { - resolve({ id: 1 }); - }) - ); - } - ); - - QUnit.test('error handler gets called for errors in validation hooks', function (assert) { - assert.expect(25); - let setupShouldBeEntered = false; - let expectedReason = { reason: 'No funciona, mon frere.' }; - - function throwAnError() { - return reject(expectedReason); - } - - routes = { - index: createHandler('index', { - beforeModel: throwAnError, - model: throwAnError, - afterModel: throwAnError, - - events: { - error: function (reason: string) { - assert.equal( - reason, - expectedReason, - "the value passed to the error handler is what was 'thrown' from the hook" - ); - }, - }, - - setup: function () { - assert.ok(setupShouldBeEntered, 'setup should be entered at this time'); - }, - }), - - about: createHandler('about', { - setup: function () { - assert.ok(true, "about handler's setup function was called"); - }, - }), - }; - - function testStartup(assert: Assert) { - map(assert, function (match) { - match('/').to('index'); - match('/about').to('about'); - }); - - // Perform a redirect on startup. - return router.handleURL('/').then(null, function (reason: string) { - assert.equal( - reason, - expectedReason, - 'handleURL error reason is what was originally thrown' - ); - - return router - .transitionTo('index') - .then(shouldNotHappen(assert), function (newReason: string) { - assert.equal( - newReason, - expectedReason, - 'transitionTo error reason is what was originally thrown' - ); - }); - }); - } - - testStartup(assert) - .then(function () { - return testStartup(assert); - }) - .then(function () { - delete routes['index']!.beforeModel; - return testStartup(assert); - }) - .then(function () { - return testStartup(assert); - }) - .then(function () { - delete routes['index']!.model; - return testStartup(assert); - }) - .then(function () { - return testStartup(assert); - }) - .then(function () { - delete routes['index']!.afterModel; - setupShouldBeEntered = true; - return testStartup(assert); - }); - }); - - QUnit.test( - "Errors shouldn't be handled after proceeding to next child route", - async function (assert) { - assert.expect(3); - - map(assert, function (match) { - match('/parent').to('parent', function (match) { - match('/articles').to('articles'); - match('/login').to('login'); - }); - }); - - routes = { - articles: createHandler('articles', { - beforeModel: function () { - assert.ok(true, 'articles beforeModel was entered'); - return Promise.reject('blorg in beforeModel in articles'); - }, - events: { - error: function () { - assert.ok(true, 'error handled in articles'); - router.transitionTo('login'); - }, - }, - }), - - login: createHandler('login', { - setup: function () { - assert.ok(true, 'login#setup'); - }, - }), - - parent: createHandler('parent', { - events: { - error: function () { - assert.ok(false, "handled error shouldn't bubble up to parent route"); - }, - }, - }), - }; - - ignoreTransitionError(router.handleURL('/parent/articles')); - } - ); - - QUnit.test( - "Error handling shouldn't trigger for transitions that are already aborted", - function (assert) { - assert.expect(1); - - map(assert, function (match) { - match('/slow_failure').to('slow_failure'); - match('/good').to('good'); - }); - - routes = { - slow_failure: createHandler('showFailure', { - model: function () { - return new Promise(function (_res, rej) { - router.transitionTo('good'); - rej(); - }); - }, - events: { - error: function () { - assert.ok(false, "error handling shouldn't fire"); - }, - }, - }), - - good: createHandler('good', { - setup: function () { - assert.ok(true, 'good#setup'); - }, - }), - }; - - return ignoreTransitionError(router.handleURL('/slow_failure')); - } - ); - - QUnit.test( - 'Transitions to the same destination as the active transition just return the active transition', - function (assert) { - assert.expect(1); - - let transition0 = router.handleURL('/index'); - let transition1 = router.handleURL('/index'); - assert.equal(transition0, transition1); - } - ); - - QUnit.test('can redirect from error handler', function (assert) { - assert.expect(4); - - let errorCount = 0; - - routes = { - index: createHandler('index'), - - showPost: createHandler('showPost', { - model: function () { - return reject('borf!'); - }, - events: { - error: function (e: Error) { - errorCount++; - - assert.equal(e, 'borf!', 'received error thrown from model'); - - // Redirect to index. - router.transitionTo('index').then(function () { - if (errorCount === 1) { - // transition back here to test transitionTo error handling. - return router - .transitionTo('showPost', reject('borf!')) - .then(shouldNotHappen(assert), function (e: Error) { - assert.equal(e, 'borf!', 'got thing'); - }); - } - return; - }, shouldNotHappen(assert)); - }, - }, - - setup: function () { - assert.ok(false, 'should not get here'); - }, - }), - }; - - router.handleURL('/posts/123').then(shouldNotHappen(assert), function (reason: string) { - assert.equal(reason, 'borf!', 'expected reason received from first failed transition'); - }); - }); - - QUnit.test('can redirect from setup/enter', function (assert) { - assert.expect(5); - - routes = { - index: createHandler('index', { - enter: function () { - assert.ok(true, 'index#enter called'); - router.transitionTo('about').then(secondAttempt, shouldNotHappen(assert)); - }, - setup: function () { - assert.ok(true, 'index#setup called'); - router.transitionTo('/about').then(thirdAttempt, shouldNotHappen(assert)); - }, - events: { - error: function () { - assert.ok(false, 'redirects should not call error hook'); - }, - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about#setup was entered'); - }, - }), - }; - - router.handleURL('/index').then(shouldNotHappen(assert), assertAbort(assert)); - - function secondAttempt() { - delete routes['index']!.enter; - router.transitionTo('index').then(shouldNotHappen(assert), assertAbort(assert)); - } - - function thirdAttempt() { - delete routes['index']!.setup; - router.transitionTo('index').then(null, shouldNotHappen(assert)); - } - }); - - QUnit.test( - 'redirecting to self from validation hooks should no-op (and not infinite loop)', - function (assert) { - assert.expect(2); - - let count = 0; - - routes = { - index: createHandler('index', { - afterModel: function () { - if (count++ > 10) { - assert.ok(false, 'infinite loop occurring'); - } else { - assert.ok(count <= 2, 'running index no more than twice'); - router.transitionTo('index'); - } - }, - setup: function () { - assert.ok(true, 'setup was called'); - }, - }), - }; - - router.handleURL('/index'); - } - ); - - QUnit.test('Transition#method(null) prevents URLs from updating', async function (assert) { - assert.expect(1); - - routes = { - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about#setup was called'); - }, - }), - }; - - router.updateURL = function () { - assert.ok(false, "updateURL shouldn't have been called"); - }; - - // Test multiple calls to method in a row. - router.handleURL('/index').method(null); - await router.handleURL('/index').method(null); - - await router.transitionTo('about').method(null); - }); - - QUnit.test( - 'redirecting to self from enter hooks should no-op (and not infinite loop)', - function (assert) { - assert.expect(1); - - let count = 0; - - routes = { - index: createHandler('index', { - setup: function () { - if (count++ > 10) { - assert.ok(false, 'infinite loop occurring'); - } else { - assert.ok(true, 'setup was called'); - router.transitionTo('index'); - } - }, - }), - }; - - router.handleURL('/index'); - } - ); - - QUnit.test( - 'redirecting to child handler from validation hooks should no-op (and not infinite loop)', - function (assert) { - assert.expect(4); - - routes = { - postIndex: createHandler('postIndex', { - beforeModel: function () { - assert.ok(true, 'postIndex beforeModel called'); - router.transitionTo('showAllPosts'); - }, - }), - - showAllPosts: createHandler('showAllPosts', { - beforeModel: function () { - assert.ok(true, 'showAllPosts beforeModel called'); - }, - }), - - showPopularPosts: createHandler('showPopularPosts', { - beforeModel: function () { - assert.ok(true, 'showPopularPosts beforeModel called'); - }, - }), - }; - - router.handleURL('/posts/popular').then( - function () { - assert.ok(false, 'redirected handleURL should not succeed'); - }, - function () { - assert.ok(true, 'redirected handleURL should fail'); - } - ); - } - ); - - function startUpSetup(assert: Assert) { - routes = { - index: createHandler('index', { - setup: function () { - assert.ok(true, 'index setup called'); - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - }), - faq: createHandler('faq', { - setup: function () { - assert.ok(true, 'faq setup called'); - }, - }), - }; - } - - QUnit.test('transitionTo with named transition can be called at startup', function (assert) { - assert.expect(2); - - startUpSetup(assert); - - router.transitionTo('index').then( - function () { - assert.ok(true, 'success handler called'); - }, - function () { - assert.ok(false, 'failure handle should not be called'); - } - ); - }); - - QUnit.test('transitionTo with URL transition can be called at startup', function (assert) { - assert.expect(2); - - startUpSetup(assert); - - router.transitionTo('/index').then( - function () { - assert.ok(true, 'success handler called'); - }, - function () { - assert.ok(false, 'failure handle should not be called'); - } - ); - }); - - QUnit.test('transitions fire a didTransition event on the destination route', function (assert) { - assert.expect(1); - - routes = { - about: createHandler('about', { - events: { - didTransition: function () { - assert.ok(true, "index's didTransition was called"); - }, - }, - }), - }; - - router.handleURL('/index').then(function () { - router.transitionTo('about'); - }, shouldNotHappen(assert)); - }); - - QUnit.test('willTransition function fired before route change', function (assert) { - assert.expect(1); - - let beforeModelNotCalled = true; - - routes = { - about: createHandler('about', { - beforeModel: function () { - beforeModelNotCalled = false; - }, - }), - }; - - router.willTransition = function () { - assert.ok(beforeModelNotCalled, 'about beforeModel hook should not be called at this time'); - }; - - router.handleURL('/about'); - }); - - QUnit.test('willTransition function fired with handler infos passed in', function (assert) { - assert.expect(2); - - router.handleURL('/about').then(function () { - router.willTransition = function (fromInfos, toInfos) { - assert.equal( - routePath(fromInfos), - 'about', - 'first argument should be the old handler infos' - ); - assert.equal( - routePath(toInfos), - 'postIndex.showPopularPosts', - 'second argument should be the new handler infos' - ); - }; - - router.handleURL('/posts/popular'); - }); - }); - - QUnit.test( - 'willTransition function fired with cancellable transition passed in', - function (assert) { - assert.expect(2); - - router.handleURL('/index').then(function () { - router.willTransition = function (_fromInfos, _toInfos, transition) { - assert.ok(true, "index's transitionTo was called"); - transition.abort(); - }; - - return router.transitionTo('about').then(shouldNotHappen(assert), assertAbort(assert)); - }); - } - ); - - QUnit.test('transitions can be aborted in the willTransition event', function (assert) { - assert.expect(3); - - routes = { - index: createHandler('index', { - setup: function () { - assert.ok(true, 'index setup called'); - }, - events: { - willTransition: function (transition: Transition) { - assert.ok(true, "index's transitionTo was called"); - transition.abort(); - }, - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - }), - }; - - router.handleURL('/index').then(function () { - return router.transitionTo('about').then(shouldNotHappen(assert), assertAbort(assert)); - }); - }); - - QUnit.test('transitions can redirected in the willTransition event', function (assert) { - assert.expect(2); - - let destFlag = true; - - routes = { - index: createHandler('index', { - setup: function () { - assert.ok(true, 'index setup called'); - }, - events: { - willTransition: function () { - // Router code must be careful here not to refire - // `willTransition` when a transition is already - // underway, else infinite loop. - let dest = destFlag ? 'about' : 'faq'; - destFlag = !destFlag; - router.transitionTo(dest); - }, - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - }), - faq: createHandler('faq', { - setup: function () { - assert.ok(false, 'faq setup should not be called'); - }, - }), - }; - - router.handleURL('/index').then(function () { - router.transitionTo('faq'); - }); - }); - - QUnit.test('transitions that abort and enter into a substate', function (assert) { - assert.expect(3); - - routes = { - index: createHandler('index'), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - events: { - willTransition: function (transition: Transition) { - assert.ok(true, 'willTransition'); - transition.abort(); - router.intermediateTransitionTo('faq'); - }, - }, - }), - faq: createHandler('faq', { - setup: function () { - assert.ok(true, 'faq setup called'); - }, - }), - }; - - router.handleURL('/about').then(() => { - return router.transitionTo('index'); - }); - }); - - QUnit.test('aborted transitions can be saved and later retried', function (assert) { - assert.expect(9); - - let shouldPrevent = true, - transitionToAbout, - lastTransition: Transition, - retryTransition: Transition; - - routes = { - index: createHandler('index', { - setup: function () { - assert.ok(true, 'index setup called'); - }, - events: { - willTransition: function (transition: Transition) { - assert.ok(true, "index's willTransition was called"); - if (shouldPrevent) { - transition.data['foo'] = 'hello'; - (transition as any).foo = 'hello'; - transition.abort(); - lastTransition = transition; - } else { - assert.notOk((transition as any).foo, 'no foo property exists on new transition'); - assert.equal( - transition.data['foo'], - 'hello', - 'values stored in data hash of old transition persist when retried' - ); - } - }, - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - }), - }; - - router.handleURL('/index').then(function () { - router - .transitionTo('about') - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'transition was blocked'); - shouldPrevent = false; - transitionToAbout = lastTransition; - retryTransition = transitionToAbout.retry(); - assert.equal(retryTransition.urlMethod, 'update'); - return retryTransition; - }) - .then(function () { - assert.ok(true, 'transition succeeded via .retry()'); - }, shouldNotHappen(assert)); - }); - }); - - QUnit.test( - 'aborted transitions can be saved and later retried asynchronously', - function (assert) { - assert.expect(2); - - let abortedTransition: Transition; - let shouldPrevent = true; - routes = { - index: createHandler('index', { - events: { - willTransition: function (transition: Transition) { - if (shouldPrevent) { - abortedTransition = transition.abort(); - - const t = router.intermediateTransitionTo('loading'); - assert.rejects(t as unknown as Promise); - } - }, - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - }), - loading: createHandler('loading', { - setup: function () { - assert.ok(true, 'loading setup called'); - }, - }), - }; - - router.handleURL('/index').then(function () { - return router - .transitionTo('about') - .then(shouldNotHappen(assert), function () { - shouldPrevent = false; - return new Promise((resolve) => { - let transition = abortedTransition.retry(); - resolve(transition); - }); - }) - .then(function () { - assert.ok(true, 'transition succeeded via .retry()'); - }, shouldNotHappen(assert)) - .catch(shouldNotHappen(assert)); - }); - } - ); - - QUnit.test( - 'if an aborted transition is retried, it preserves the urlMethod of the original one', - function (assert) { - assert.expect(9); - - let shouldPrevent = true, - transitionToAbout, - lastTransition: Transition, - retryTransition: Transition; - - routes = { - index: createHandler('index', { - setup: function () { - assert.ok(true, 'index setup called'); - }, - events: { - willTransition: function (transition: Transition) { - assert.ok(true, "index's willTransition was called"); - if (shouldPrevent) { - transition.data['foo'] = 'hello'; - (transition as any).foo = 'hello'; - transition.abort(); - lastTransition = transition; - } else { - assert.notOk((transition as any).foo, 'no foo property exists on new transition'); - assert.equal( - transition.data['foo'], - 'hello', - 'values stored in data hash of old transition persist when retried' - ); - } - }, - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'about setup called'); - }, - }), - }; - - router.handleURL('/index').then(function () { - router - .replaceWith('about') - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'transition was blocked'); - shouldPrevent = false; - transitionToAbout = lastTransition; - retryTransition = transitionToAbout.retry(); - assert.equal(retryTransition.urlMethod, 'replace'); - return transitionToAbout.retry(); - }) - .then(function () { - assert.ok(true, 'transition succeeded via .retry()'); - }, shouldNotHappen(assert)); - }); - } - ); - - QUnit.test( - 'if an initial transition is aborted during validation phase and later retried', - async function (assert) { - assert.expect(7); - - let shouldRedirectToLogin = true; - let currentURL = '/login'; - let urlStack: string[][] = []; - let lastTransition: Transition; - - map(assert, function (match) { - match('/').to('index'); - match('/login').to('login'); - }); - - router.updateURL = function (url) { - urlStack.push(['updateURL', url]); - currentURL = url; - }; - - router.replaceURL = function (url) { - urlStack.push(['replaceURL', url]); - currentURL = url; - }; - - routes = { - index: createHandler('index', { - beforeModel: function (transition: Transition) { - assert.ok(true, 'index model called'); - if (shouldRedirectToLogin) { - lastTransition = transition; - return router.transitionTo('/login'); - } - return; - }, - }), - login: createHandler('login', { - setup: function () { - assert.ok('login setup called'); - }, - }), - }; - - // use `handleURL` to emulate initial transition properly - try { - await router.handleURL('/'); - shouldNotHappen(assert, 'initial transition aborted'); - } catch { - assert.equal(currentURL, '/login', 'currentURL matches on initial transition aborted'); - assert.deepEqual(urlStack, [['replaceURL', '/login']]); - - shouldRedirectToLogin = false; - await lastTransition!.retry(); - assert.equal(currentURL, '/', 'after retry currentURL is updated'); - assert.deepEqual(urlStack, [ - ['replaceURL', '/login'], - ['updateURL', '/'], - ]); - } - } - ); - - QUnit.test('completed transitions can be saved and later retried', function (assert) { - assert.expect(8); - - let post = { id: '123' }, - savedTransition: Transition; - - routes = { - showPost: createHandler('showPost', { - afterModel: function (model: Dict, transition: Transition) { - if (savedTransition === undefined) { - assert.equal(transition.from && transition.from.localName, 'index', 'starting point'); - } else { - assert.equal( - transition.from && transition.from.localName, - 'about', - 'new starting point' - ); - } - - assert.equal(transition.to && transition.to.localName, 'showPost', 'to points at leaf'); - assert.equal(model, post, "showPost's afterModel got the expected post model"); - savedTransition = transition; - }, - }), - index: createHandler('index', { - model(_params: Dict, transition: Transition) { - assert.equal(transition.from, null); - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(true, 'setup was entered'); - }, - }), - }; - - router - .handleURL('/index') - .then(function () { - return router.transitionTo('showPost', post); - }) - .then(function () { - return router.transitionTo('about'); - }) - .then(function () { - return savedTransition.retry(); - }); - }); - - function setupAuthenticatedExample(assert: Assert) { - map(assert, function (match) { - match('/index').to('index'); - match('/login').to('login'); - - match('/admin').to('admin', function (match) { - match('/about').to('about'); - match('/posts/:post_id').to('adminPost'); - }); - }); - - let isLoggedIn = false, - lastRedirectedTransition: Transition; - - routes = { - index: createHandler('index'), - login: createHandler('login', { - events: { - logUserIn: function () { - isLoggedIn = true; - lastRedirectedTransition.retry(); - }, - }, - }), - admin: createHandler('admin', { - beforeModel: function (transition: Transition) { - lastRedirectedTransition = transition; - assert.ok(true, 'beforeModel redirect was called'); - if (!isLoggedIn) { - router.transitionTo('login'); - } - }, - }), - about: createHandler('about', { - setup: function () { - assert.ok(isLoggedIn, 'about was entered only after user logged in'); - }, - }), - adminPost: createHandler('adminPost', { - model: function (params: Dict) { - assert.deepEqual( - params, - { post_id: '5', queryParams: {} }, - 'adminPost received params previous transition attempt' - ); - return 'adminPost'; - }, - setup: function (model: Dict) { - assert.equal(model, 'adminPost', 'adminPost was entered with correct model'); - }, - }), - }; - } - - QUnit.test('authenticated routes: starting on non-auth route', async function (assert) { - assert.expect(8); - - setupAuthenticatedExample(assert); - - await router.transitionTo('/index'); - await transitionToWithAbort(assert, router, 'about'); - await transitionToWithAbort(assert, router, 'about'); - await transitionToWithAbort(assert, router, '/admin/about'); - - // Log in. This will retry the last failed transition to 'about'. - router.trigger('logUserIn'); - }); - - QUnit.test('authenticated routes: starting on auth route', async function (assert) { - assert.expect(8); - - setupAuthenticatedExample(assert); - - await transitionToWithAbort(assert, router, '/admin/about'); - await transitionToWithAbort(assert, router, '/admin/about'); - await transitionToWithAbort(assert, router, 'about'); - - // Log in. This will retry the last failed transition to 'about'. - router.trigger('logUserIn'); - }); - - QUnit.test('authenticated routes: starting on parameterized auth route', async function (assert) { - assert.expect(5); - - setupAuthenticatedExample(assert); - - await transitionToWithAbort(assert, router, '/admin/posts/5'); - - // Log in. This will retry the last failed transition to '/posts/5'. - router.trigger('logUserIn'); - }); - - QUnit.test('An instantly aborted transition fires no hooks', function (assert) { - assert.expect(8); - - let hooksShouldBeCalled = false; - - routes = { - index: createHandler('index', { - beforeModel: function (transition: Transition) { - assert.equal( - transition.from, - null, - 'from is "null" on initial transitions even with aborts' - ); - assert.ok(hooksShouldBeCalled, 'index beforeModel hook should be called at this time'); - }, - }), - about: createHandler('about', { - beforeModel: function () { - assert.ok(hooksShouldBeCalled, 'about beforeModel hook should be called at this time'); - }, - }), - }; - - router - .transitionTo('index') - .abort() - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'Failure handler called for index'); - return router.transitionTo('/index').abort(); - }) - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'Failure handler called for /index'); - hooksShouldBeCalled = true; - return router.transitionTo('index'); - }) - .then(function () { - assert.ok(true, 'Success handler called for index'); - hooksShouldBeCalled = false; - return router.transitionTo('about').abort(); - }, shouldNotHappen(assert)) - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'failure handler called for about'); - return router.transitionTo('/about').abort(); - }) - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'failure handler called for /about'); - hooksShouldBeCalled = true; - return router.transitionTo('/about'); - }); - }); - - QUnit.test('a successful transition resolves with the target handler', function (assert) { - assert.expect(2); - - // Note: this is extra convenient for Ember where you can all - // .transitionTo right on the route. - - routes = { - index: createHandler('index', { borfIndex: true }), - about: createHandler('about', { borfAbout: true }), - }; - - router - .handleURL('/index') - .then(function (route: Route) { - assert.ok((route as any)['borfIndex'], 'resolved to index handler'); - return router.transitionTo('about'); - }, shouldNotHappen(assert)) - .then(function (result: Dict) { - assert.ok(result['borfAbout'], 'resolved to about handler'); - }); - }); - - QUnit.test('transitions have a .promise property', function (assert) { - assert.expect(2); - - router - .handleURL('/index') - .promise!.then(function () { - let promise = router.transitionTo('about').abort().promise; - assert.ok(promise, 'promise exists on aborted transitions'); - return promise; - }, shouldNotHappen(assert)) - .then(shouldNotHappen(assert), function () { - assert.ok(true, 'failure handler called'); - }); - }); - - QUnit.test( - 'the serialize function is bound to the correct object when called', - function (assert) { - assert.expect(scenario.async ? 0 : 1); - - routes = { - showPostsForDate: createHandler('showPostsForDate', { - serialize: function (date: any) { - assert.equal(this, routes['showPostsForDate']); - return { - date: date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate(), - }; - }, - }), - }; - - router.generate('showPostsForDate', new Date(1815, 5, 18)); - } - ); - - QUnit.test( - 'transitionTo will soak up resolved parent models of active transition', - function (assert) { - assert.expect(5); - - let admin = { id: 47 }, - adminPost = { id: 74 }, - adminSetupShouldBeEntered = false; - - function adminPromise() { - return new Promise(function (res) { - res(admin); - }); - } - - let adminHandler = createHandler('admin', { - serialize: function (object: Dict) { - assert.equal(object['id'], 47, 'The object passed to serialize is correct'); - return { id: 47 }; - }, - - model: function (params: Dict) { - assert.equal(params['id'], 47, 'The object passed to serialize is correct'); - return admin; - }, - - setup: function () { - assert.ok( - adminSetupShouldBeEntered, - "adminHandler's setup should be called at this time" - ); - }, - }); - - let adminPostHandler = createHandler('adminPost', { - serialize: function (object: Dict) { - return { post_id: object['id'] }; - }, - - setup: function () { - assert.equal( - adminHandler.context, - admin, - 'adminPostHandler receives resolved soaked promise from previous transition' - ); - }, - - model: function () { - return adminPost; - }, - }); - - let adminPostsHandler = createHandler('adminPosts', { - beforeModel: function () { - adminSetupShouldBeEntered = true; - router.transitionTo('adminPost', adminPost); - }, - }); - - let indexHandler = createHandler('index', { - setup: function () { - assert.ok(true, 'index entered'); - }, - }); - - routes = { - index: indexHandler, - admin: adminHandler, - adminPost: adminPostHandler, - adminPosts: adminPostsHandler, - }; - - router.transitionTo('index').then(function () { - router - .transitionTo('adminPosts', adminPromise()) - .then(shouldNotHappen(assert), assertAbort(assert)); - }); - } - ); - - QUnit.test( - "transitionTo will soak up resolved all models of active transition, including present route's resolved model", - function (assert) { - assert.expect(2); - - let modelCalled = 0, - hasRedirected = false; - - map(assert, function (match) { - match('/post').to('post', function (match) { - match('/').to('postIndex'); - match('/new').to('postNew'); - }); - }); - - let postHandler = createHandler('post', { - model: function () { - assert.equal(modelCalled++, 0, "postHandler's model should only be called once"); - return { title: 'Hello world' }; - }, - - redirect: function () { - if (!hasRedirected) { - hasRedirected = true; - router.transitionTo('postNew'); - } - }, - }); - - routes = { - post: postHandler, - postIndex: createHandler('postIndex'), - postNew: createHandler('postNew'), - }; - - router.transitionTo('postIndex').then(shouldNotHappen(assert), assertAbort(assert)); - } - ); - - QUnit.test("can reference leaf '/' route by leaf or parent name", async function (assert) { - map(assert, function (match) { - match('/').to('app', function (match) { - match('/').to('index'); - match('/nest').to('nest', function (match) { - match('/').to('nest.index'); - }); - }); - }); - - function assertOnRoute(name: string) { - let last = router.currentRouteInfos![router.currentRouteInfos!.length - 1]; - assert.equal(last!.name, name); - } - - await router.transitionTo('app'); - assertOnRoute('index'); - await router.transitionTo('nest'); - assertOnRoute('nest.index'); - await router.transitionTo('app'); - assertOnRoute('index'); - }); - - QUnit.test('resolved models can be swapped out within afterModel', function (assert) { - assert.expect(3); - - let modelPre = {}, - modelPost = {}; - - routes = { - index: createHandler('index', { - model: function () { - return modelPre; - }, - afterModel: function (resolvedModel: Dict, transition: Transition) { - assert.equal( - resolvedModel, - transition.resolvedModels['index'], - "passed-in resolved model equals model in transition's hash" - ); - assert.equal( - resolvedModel, - modelPre, - 'passed-in resolved model equals model returned from `model`' - ); - transition.resolvedModels['index'] = modelPost; - }, - setup: function (model: Dict) { - assert.equal( - model, - modelPost, - 'the model passed to `setup` is the one substituted in afterModel' - ); - }, - }), - }; - - router.transitionTo('index'); - }); - - QUnit.test('String/number args in transitionTo are treated as url params', function (assert) { - assert.expect(11); - - let adminParams = { id: '1' }, - adminModel = { id: '1' }, - adminPostModel = { id: '2' }; - - routes = { - admin: createHandler('admin', { - model: function (params: Dict) { - delete params['queryParams']; - assert.deepEqual( - params, - adminParams, - 'admin handler gets the number passed in via transitionTo, converts to string' - ); - return adminModel; - }, - }), - adminPost: createHandler('adminPost', { - model: function (params: Dict) { - delete params['queryParams']; - assert.deepEqual( - params, - { post_id: '2' }, - 'adminPost handler gets the string passed in via transitionTo' - ); - return adminPostModel; - }, - setup: function () { - assert.ok(true, 'adminPost setup was entered'); - }, - }), - }; - - router - .handleURL('/index') - .then(function () { - expectedUrl = '/posts/admin/1/posts/2'; - return router.transitionTo('adminPost', 1, '2'); - }) - .then(function () { - assert.ok(router.isActive('adminPost', 1, '2'), 'adminPost is active via params'); - assert.ok( - router.isActive('adminPost', 1, adminPostModel), - 'adminPost is active via contexts' - ); - - adminParams = { id: '0' }; - expectedUrl = '/posts/admin/0/posts/2'; - return router.transitionTo('adminPost', 0, '2'); - }) - .then(function () { - assert.ok(router.isActive('adminPost', 0, '2'), 'adminPost is active via params'); - assert.ok( - router.isActive('adminPost', 0, adminPostModel), - 'adminPost is active via contexts' - ); - }, shouldNotHappen(assert)); - }); - - QUnit.test( - "Transitions returned from beforeModel/model/afterModel hooks aren't treated as pausing promises", - function (assert) { - assert.expect(6); - - routes = { - index: createHandler('index', { - beforeModel: function () { - assert.ok(true, 'index beforeModel called'); - return router.transitionTo('index'); - }, - model: function () { - assert.ok(true, 'index model called'); - return router.transitionTo('index'); - }, - afterModel: function () { - assert.ok(true, 'index afterModel called'); - return router.transitionTo('index'); - }, - }), - }; - - function testStartup(assert: Assert) { - map(assert, function (match) { - match('/index').to('index'); - }); - - return router.handleURL('/index'); - } - - testStartup(assert) - .then(function () { - delete routes['index']!.beforeModel; - return testStartup(assert); - }) - .then(function () { - delete routes['index']!.model; - return testStartup(assert); - }) - .then(function () { - delete routes['index']!.afterModel; - return testStartup(assert); - }); - } - ); - - /* TODO: revisit this idea - QUnit.test("exceptions thrown from model hooks aren't swallowed", function(assert) { - assert.expect(7); - - enableErrorHandlingDeferredActionQueue(); - - let anError = {}; - function throwAnError() { - throw anError; - } - - let routeWasEntered = false; - - handlers = { - index: { - beforeModel: throwAnError, - model: throwAnError, - afterModel: throwAnError, - setup: function(model) { - routeWasEntered = true; - } - } - }; - - let hooks = ['beforeModel', 'model', 'afterModel']; - - while(hooks.length) { - let transition = router.transitionTo('index'); - flush(anError); - transition.abort(); - assert.ok(!routeWasEntered, "route hasn't been entered yet"); - delete handlers.index[hooks.shift()]; - } - - router.transitionTo('index'); - flush(anError); - - assert.ok(routeWasEntered, "route was finally entered"); - }); - */ - - QUnit.test( - 'Transition#followRedirects() returns a promise that fulfills when any redirecting transitions complete', - function (assert) { - assert.expect(3); - - routes['about'] = createHandler('about', { - redirect: function () { - router.transitionTo('faq').then(null, shouldNotHappen(assert)); - }, - }); - - router - .transitionTo('/index') - .followRedirects() - .then(function (handler: Route) { - assert.equal( - handler, - routes['index'], - 'followRedirects works with non-redirecting transitions' - ); - - return router.transitionTo('about').followRedirects(); - }) - .then(function (handler: Route) { - assert.equal( - handler, - routes['faq'], - 'followRedirects promise resolved with redirected faq handler' - ); - - (routes['about'] as Route).beforeModel = function (transition: Transition) { - transition.abort(); - return undefined; - }; - - // followRedirects should just reject for non-redirecting transitions. - return router - .transitionTo('about') - .followRedirects() - .then(shouldNotHappen(assert), assertAbort(assert)); - }); - } - ); - - QUnit.test( - 'Transition#followRedirects() works correctly when redirecting from an async model hook', - function (assert) { - assert.expect(2); - - routes['index'] = createHandler('index', { - beforeModel: function () { - return Promise.resolve(true).then(() => { - return router.transitionTo('about'); - }); - }, - }); - - routes['about'] = createHandler('about', { - setup: function () { - assert.ok(true, 'about#setup was called'); - }, - }); - - router - .transitionTo('/index') - .followRedirects() - .then(function (handler: Route) { - assert.equal( - handler, - routes['about'], - 'followRedirects works with redirect from async hook transitions' - ); - }); - } - ); - - QUnit.test( - "Returning a redirecting Transition from a model hook doesn't cause things to explode", - function (assert) { - assert.expect(2); - - routes['index'] = createHandler('index', { - beforeModel: function () { - return router.transitionTo('about'); - }, - }); - - routes['about'] = createHandler('about', { - setup: function () { - assert.ok(true, 'about#setup was called'); - }, - }); - - router.transitionTo('/index').then(null, assertAbort(assert)); - } - ); - - QUnit.test('Generate works w queryparams', function (assert) { - assert.equal(router.generate('index'), '/index', 'just index'); - assert.equal( - router.generate('index', { queryParams: { foo: '123' } }), - '/index?foo=123', - 'just index' - ); - assert.equal( - router.generate('index', { queryParams: { foo: '123', bar: '456' } }), - '/index?bar=456&foo=123', - 'just index' - ); - }); - - if (scenario.async) { - QUnit.test('Generate does not invoke getHandler', function (assert) { - let originalGetHandler = router.getRoute; - router.getRoute = function () { - assert.ok(false, 'getHandler should not be called'); - return createHandler('empty'); - }; - - assert.equal(router.generate('index'), '/index', 'just index'); - assert.equal( - router.generate('index', { queryParams: { foo: '123' } }), - '/index?foo=123', - 'just index' - ); - assert.equal( - router.generate('index', { queryParams: { foo: '123', bar: '456' } }), - '/index?bar=456&foo=123', - 'just index' - ); - - router.getRoute = originalGetHandler; - }); - } - - QUnit.test('errors in enter/setup hooks fire `error`', function (assert) { - assert.expect(4); - - let count = 0; - - routes = { - index: createHandler('index', { - enter: function () { - throw 'OMG ENTER'; - }, - setup: function () { - throw 'OMG SETUP'; - }, - events: { - error: function (e: Error) { - if (count === 0) { - assert.equal(e, 'OMG ENTER', "enter's throw value passed to error hook"); - } else if (count === 1) { - assert.equal(e, 'OMG SETUP', "setup's throw value passed to error hook"); - } else { - assert.ok(false, 'should not happen'); - } - }, - }, - }), - }; - - router - .handleURL('/index') - .then(shouldNotHappen(assert), function (reason: string) { - assert.equal(reason, 'OMG ENTER', "enters's error was propagated"); - count++; - delete routes['index']!.enter; - return router.handleURL('/index'); - }) - .then(shouldNotHappen(assert), function (reason: string) { - assert.equal(reason, 'OMG SETUP', "setup's error was propagated"); - delete routes['index']!.setup; - }); - }); - - QUnit.test( - 'invalidating parent model with different string/numeric parameters invalidates children', - async function (assert) { - map(assert, function (match) { - match('/:p').to('parent', function (match) { - match('/:c').to('child'); - }); - }); - - assert.expect(8); - - let count = 0; - routes = { - parent: createHandler('parent', { - model: function (params: Dict) { - assert.ok(true, 'parent model called'); - return { id: params['p'] }; - }, - setup: function (model: Dict) { - if (count === 0) { - assert.deepEqual(model, { id: '1' }); - } else { - assert.deepEqual(model, { id: '2' }); - } - }, - }), - child: createHandler('child', { - model: function (params: Dict) { - assert.ok(true, 'child model called'); - return { id: params['c'] }; - }, - setup: function (model: Dict) { - if (count === 0) { - assert.deepEqual(model, { id: '1' }); - } else { - assert.deepEqual(model, { id: '1' }); - } - }, - }), - }; - - await router.transitionTo('child', '1', '1'); - count = 1; - await router.transitionTo('child', '2', '1'); - } - ); - - QUnit.test( - 'intents make use of previous transition state in case not enough contexts are provided to retry a transition', - async function (assert) { - assert.expect(3); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/users/:user').to('user', function (match) { - match('/index').to('userIndex'); - match('/auth').to('auth'); - }); - match('/login').to('login'); - }); - }); - - let hasAuthed = false, - savedTransition: Transition, - didFinish = false; - routes = { - auth: createHandler('auth', { - beforeModel: function (transition: Transition) { - if (!hasAuthed) { - savedTransition = transition; - router.transitionTo('login'); - } - }, - setup: function () { - didFinish = true; - }, - }), - }; - - await router.transitionTo('userIndex', { user: 'machty' }); - - // Then attempt to transition into auth; this will redirect. - await ignoreTransitionError(router.transitionTo('auth')); - - assert.ok(savedTransition!, 'transition was saved'); - - hasAuthed = true; - await savedTransition!.retry(); - - assert.ok(didFinish, 'did enter auth route'); - assert.equal( - (routes['user']!.context as any).user, - 'machty', - 'User was remembered upon retry' - ); - } - ); - - QUnit.test('A failed transition calls the catch and finally callbacks', function (assert) { - assert.expect(2); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/bad').to('badRoute'); - }); - }); - - routes = { - badRoute: createHandler('badRoute', { - beforeModel: function () { - return new Promise(function (_resolve, reject) { - reject('example reason'); - }); - }, - }), - }; - - return router - .handleURL('/bad') - .catch(function () { - assert.ok(true, 'catch callback was called'); - }) - .finally(function () { - assert.ok(true, 'finally callback was called'); - }); - }); - - QUnit.test('A successful transition calls the finally callback', function (assert) { - assert.expect(1); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/example').to('exampleRoute'); - }); - }); - - router.handleURL('/example').finally(function () { - assert.ok(true, 'finally callback was called'); - }); - }); - - QUnit.test('transition sets isActive by default', function (assert) { - assert.expect(2); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/example').to('exampleRoute'); - }); - }); - - let transition = router.handleURL('/example'); - - assert.true(transition.isActive); - assert.false(transition.isAborted); - }); - - QUnit.test('transition sets isActive to false when aborted', function (assert) { - assert.expect(4); - - map(assert, function (match) { - match('/').to('application', function (match) { - match('/example').to('exampleRoute'); - }); - }); - - let transition = router.handleURL('/example'); - - assert.true(transition.isActive, 'precond'); - assert.false(transition.isAborted, 'precond'); - - transition.abort(); - - assert.false(transition.isActive, 'isActive should be false after abort'); - assert.true(transition.isAborted, 'isAborted is set to true after abort'); - }); - - if (scenario.async) { - QUnit.test('getHandler is invoked synchronously when returning Promises', function (assert) { - assert.expect(2); - - let count = 0; - let handlerCount = 2; - - routes = { - postIndex: createHandler('postIndex'), - showAllPosts: createHandler('showAllPosts'), - }; - - router.getRoute = function (name) { - count++; - - return Promise.resolve(scenario.getRoute.call(null, name)).then(function (handler: Route) { - assert.equal(count, handlerCount); - return handler; - }); - }; - - router.transitionTo('/posts/all'); - }); - } - - QUnit.module('Multiple dynamic segments per route (' + scenario.name + ')'); - - QUnit.test('Multiple string/number params are soaked up', async function (assert) { - assert.expect(3); - - map(assert, function (match) { - match('/:foo_id/:bar_id').to('bar'); - }); - - routes = { - bar: createHandler('bar', { - model: function () { - return {}; - }, - }), - }; - - expectedUrl = '/omg/lol'; - await router.transitionTo('bar', 'omg', 'lol'); - - expectedUrl = '/omg/heehee'; - await router.transitionTo('bar', 'heehee'); - - expectedUrl = '/lol/no'; - await router.transitionTo('bar', 'lol', 'no'); - }); - - QUnit.module('isActive (' + scenario.name + ')', { - beforeEach: async function (assert: Assert) { - routes = { - parent: createHandler('parent', { - serialize: function (obj: Dict) { - return { - one: obj['one'], - two: obj['two'], - }; - }, - }), - child: createHandler('child', { - serialize: function (obj: Dict) { - return { - three: obj['three'], - four: obj['four'], - }; - }, - }), - }; - - // When using an async getHandler serializers need to be loaded separately - if (scenario.async) { - serializers = { - parent: function (obj) { - let castObj = obj as Dict; - // TODO: Review this - return { - one: castObj['one'], - two: castObj['two'], - }; - }, - child: function (obj) { - let castObj = obj as Dict; - return { - three: castObj['three'], - four: castObj['four'], - }; - }, - }; - } - - map(assert, function (match) { - match('/:one/:two').to('parent', function (match) { - match('/:three/:four').to('child'); - }); - }); - - expectedUrl = null; - - await router.transitionTo('child', 'a', 'b', 'c', 'd'); - }, - }); - - QUnit.test( - 'isActive supports multiple soaked up string/number params (via params)', - function (assert) { - assert.ok(router.isActive('child'), 'child'); - assert.ok(router.isActive('parent'), 'parent'); - - assert.ok(router.isActive('child', 'd'), 'child d'); - assert.ok(router.isActive('child', 'c', 'd'), 'child c d'); - assert.ok(router.isActive('child', 'b', 'c', 'd'), 'child b c d'); - assert.ok(router.isActive('child', 'a', 'b', 'c', 'd'), 'child a b c d'); - - assert.notOk(router.isActive('child', 'e'), '!child e'); - assert.notOk(router.isActive('child', 'c', 'e'), '!child c e'); - assert.notOk(router.isActive('child', 'e', 'd'), '!child e d'); - assert.notOk(router.isActive('child', 'x', 'x'), '!child x x'); - assert.notOk(router.isActive('child', 'b', 'c', 'e'), '!child b c e'); - assert.notOk(router.isActive('child', 'b', 'e', 'd'), 'child b e d'); - assert.notOk(router.isActive('child', 'e', 'c', 'd'), 'child e c d'); - assert.notOk(router.isActive('child', 'a', 'b', 'c', 'e'), 'child a b c e'); - assert.notOk(router.isActive('child', 'a', 'b', 'e', 'd'), 'child a b e d'); - assert.notOk(router.isActive('child', 'a', 'e', 'c', 'd'), 'child a e c d'); - assert.notOk(router.isActive('child', 'e', 'b', 'c', 'd'), 'child e b c d'); - - assert.ok(router.isActive('parent', 'b'), 'parent b'); - assert.ok(router.isActive('parent', 'a', 'b'), 'parent a b'); - - assert.notOk(router.isActive('parent', 'c'), '!parent c'); - assert.notOk(router.isActive('parent', 'a', 'c'), '!parent a c'); - assert.notOk(router.isActive('parent', 'c', 'b'), '!parent c b'); - assert.notOk(router.isActive('parent', 'c', 't'), '!parent c t'); - } - ); - - QUnit.test( - 'isActive supports multiple soaked up string/number params (via serialized objects)', - function (assert) { - assert.ok(router.isActive('child', { three: 'c', four: 'd' }), 'child(3:c, 4:d)'); - assert.notOk(router.isActive('child', { three: 'e', four: 'd' }), '!child(3:e, 4:d)'); - assert.notOk(router.isActive('child', { three: 'c', four: 'e' }), '!child(3:c, 4:e)'); - assert.notOk(router.isActive('child', { three: 'c' }), '!child(3:c)'); - assert.notOk(router.isActive('child', { four: 'd' }), '!child(4:d)'); - assert.notOk(router.isActive('child', {}), '!child({})'); - - assert.ok(router.isActive('parent', { one: 'a', two: 'b' }), 'parent(1:a, 2:b)'); - assert.notOk(router.isActive('parent', { one: 'e', two: 'b' }), '!parent(1:e, 2:b)'); - assert.notOk(router.isActive('parent', { one: 'a', two: 'e' }), '!parent(1:a, 2:e)'); - assert.notOk(router.isActive('parent', { one: 'a' }), '!parent(1:a)'); - assert.notOk(router.isActive('parent', { two: 'b' }), '!parent(2:b)'); - - assert.ok( - router.isActive('child', { one: 'a', two: 'b' }, { three: 'c', four: 'd' }), - 'child(1:a, 2:b, 3:c, 4:d)' - ); - assert.notOk( - router.isActive('child', { one: 'e', two: 'b' }, { three: 'c', four: 'd' }), - '!child(1:e, 2:b, 3:c, 4:d)' - ); - assert.notOk( - router.isActive('child', { one: 'a', two: 'b' }, { three: 'c', four: 'e' }), - '!child(1:a, 2:b, 3:c, 4:e)' - ); - } - ); - - QUnit.test( - 'isActive supports multiple soaked up string/number params (mixed)', - function (assert) { - assert.ok(router.isActive('child', 'a', 'b', { three: 'c', four: 'd' })); - assert.ok(router.isActive('child', 'b', { three: 'c', four: 'd' })); - assert.notOk(router.isActive('child', 'a', { three: 'c', four: 'd' })); - assert.ok(router.isActive('child', { one: 'a', two: 'b' }, 'c', 'd')); - assert.ok(router.isActive('child', { one: 'a', two: 'b' }, 'd')); - assert.notOk(router.isActive('child', { one: 'a', two: 'b' }, 'c')); - - assert.notOk(router.isActive('child', 'a', 'b', { three: 'e', four: 'd' })); - assert.notOk(router.isActive('child', 'b', { three: 'e', four: 'd' })); - assert.notOk(router.isActive('child', { one: 'e', two: 'b' }, 'c', 'd')); - assert.notOk(router.isActive('child', { one: 'e', two: 'b' }, 'd')); - } - ); - - QUnit.module('Preservation of params between redirects (' + scenario.name + ')', { - beforeEach: function (assert: Assert) { - expectedUrl = null; - - map(assert, function (match) { - match('/').to('index'); - match('/:foo_id').to('foo', function (match) { - match('/').to('fooIndex'); - match('/:bar_id').to('bar', function (match) { - match('/').to('barIndex'); - }); - }); - }); - - routes = { - foo: createHandler('foo', { - modelCount: undefined, - model: function (params: Dict) { - this['modelCount'] = this['modelCount'] ? (this as any).modelCount + 1 : 1; - return { id: params['foo_id'] }; - }, - afterModel: function () { - router.transitionTo('barIndex', '789'); - }, - }), - - bar: createHandler('bar', { - model: function (params: Dict) { - this['modelCount'] = this['modelCount'] ? (this as any).modelCount + 1 : 1; - return { id: params['bar_id'] }; - }, - }), - }; - }, - }); - - QUnit.test("Starting on '/' root index", async function (assert) { - await router.transitionTo('/'); - - // Should call model for foo and bar - expectedUrl = '/123/789'; - await ignoreTransitionError(router.transitionTo('barIndex', '123', '456')); - - assert.equal( - (routes['foo'] as any).modelCount, - 2, - 'redirect in foo#afterModel should run foo#model twice (since validation failed)' - ); - - assert.deepEqual(routes['foo']!.context, { id: '123' }); - assert.deepEqual( - routes['bar']!.context, - { id: '789' }, - 'bar should have redirected to bar 789' - ); - - // Try setting foo's context to 200; this should redirect - // bar to '789' but preserve the new foo 200. - expectedUrl = '/200/789'; - await ignoreTransitionError(router.transitionTo('fooIndex', '200')); - - assert.equal( - (routes['foo'] as any).modelCount, - 4, - 'redirect in foo#afterModel should re-run foo#model' - ); - - assert.deepEqual(routes['foo']!.context, { id: '200' }); - assert.deepEqual( - routes['bar']!.context, - { id: '789' }, - 'bar should have redirected to bar 789' - ); - }); - - QUnit.test("Starting on '/' root index, using redirect", async function (assert) { - (routes['foo']!.redirect as any) = routes['foo']!.afterModel; - delete routes['foo']!.afterModel; - - await router.transitionTo('/'); - - // Should call model for foo and bar - expectedUrl = '/123/789'; - await ignoreTransitionError(router.transitionTo('barIndex', '123', '456')); - - assert.equal( - (routes['foo'] as any).modelCount, - 1, - 'redirect in foo#redirect should NOT run foo#model (since validation succeeded)' - ); - - assert.deepEqual(routes['foo']!.context, { id: '123' }); - assert.deepEqual( - routes['bar']!.context, - { id: '789' }, - 'bar should have redirected to bar 789' - ); - - // Try setting foo's context to 200; this should redirect - // bar to '789' but preserve the new foo 200. - expectedUrl = '/200/789'; - await ignoreTransitionError(router.transitionTo('fooIndex', '200')); - - assert.equal( - (routes['foo'] as any).modelCount, - 2, - 'redirect in foo#redirect should NOT foo#model' - ); - - assert.deepEqual(routes['foo']!.context, { id: '200' }); - assert.deepEqual( - routes['bar']!.context, - { id: '789' }, - 'bar should have redirected to bar 789' - ); - }); - - QUnit.test('Starting on non root index', async function (assert) { - await ignoreTransitionError(router.transitionTo('/123/456')); - assert.deepEqual(routes['foo']!.context, { id: '123' }); - assert.deepEqual( - routes['bar']!.context, - { id: '789' }, - 'bar should have redirected to bar 789' - ); - - // Try setting foo's context to 200; this should redirect - // bar to '789' but preserve the new foo 200. - expectedUrl = '/200/789'; - - await ignoreTransitionError(router.transitionTo('fooIndex', '200')); - - assert.deepEqual(routes['foo']!.context, { id: '200' }); - assert.deepEqual( - routes['bar']!.context, - { id: '789' }, - 'bar should have redirected to bar 789' - ); - }); - - /* TODO revisit - QUnit.test("A failed handler's setup shouldn't prevent future transitions", function(assert) { - assert.expect(2); - - enableErrorHandlingDeferredActionQueue(); - - map(assert, function(match) { - match("/parent").to('parent', function(match) { - match("/articles").to('articles'); - match("/login").to('login'); - }); - }); - - let error = new Error("blorg"); - - handlers = { - articles: { - setup: function() { - assert.ok(true, "articles setup was entered"); - throw error; - }, - events: { - error: function() { - assert.ok(true, "error handled in articles"); - router.transitionTo('login'); - } - } - }, - - login: { - setup: function() { - start(); - } - } - }; - - router.handleURL('/parent/articles'); - flush(error); - }); - */ - - QUnit.test( - "beforeModel shouldn't be refired with incorrect params during redirect", - async function (assert) { - // Source: https://github.com/emberjs/ember.js/issues/3407 - - assert.expect(3); - - map(assert, function (match) { - match('/').to('index'); - match('/people/:id').to('people', function (match) { - match('/').to('peopleIndex'); - match('/home').to('peopleHome'); - }); - }); - - let peopleModels: any[] = [null, {}, {}]; - let peopleBeforeModelCalled = false; - - routes = { - people: createHandler('people', { - beforeModel: function () { - assert.notOk(peopleBeforeModelCalled, 'people#beforeModel should only be called once'); - peopleBeforeModelCalled = true; - }, - model: function (params: Dict) { - assert.ok(params['id'], 'people#model called'); - return peopleModels[params['id'] as number]; - }, - }), - peopleIndex: createHandler('peopleIndex', { - afterModel: function () { - router.transitionTo('peopleHome'); - }, - }), - peopleHome: createHandler('peopleHome', { - setup: function () { - assert.ok(true, 'I was entered'); - }, - }), - }; - - await router.transitionTo('/'); - await ignoreTransitionError(router.transitionTo('peopleIndex', '1')); - } - ); - - QUnit.module('URL-less routes (' + scenario.name + ')', { - beforeEach: function (assert: Assert) { - routes = {}; - expectedUrl = null; - - map(assert, function (match) { - match('/index').to('index'); - match('/admin').to('admin', function (match) { - match('/posts').to('adminPosts'); - match('/articles').to('adminArticles'); - }); - }); - }, - }); - - QUnit.test( - "Transitioning into a route marked as inaccessibleByURL doesn't update the URL", - function (assert) { - assert.expect(1); - - routes = { - adminPosts: createHandler('adminPosts', { - inaccessibleByURL: true, - }), - }; - - router - .handleURL('/index') - .then(function () { - url = '/index'; - return router.transitionTo('adminPosts'); - }) - .then(function () { - assert.equal(url, '/index'); - }); - } - ); - - QUnit.test( - "Transitioning into a route with a parent route marked as inaccessibleByURL doesn't update the URL", - async function (assert) { - assert.expect(2); - - routes = { - admin: createHandler('admin', { - inaccessibleByURL: true, - }), - }; - - await router.transitionTo('/index'); - url = '/index'; - await router.transitionTo('adminPosts'); - assert.equal(url, '/index'); - await router.transitionTo('adminArticles'); - assert.equal(url, '/index'); - } - ); - - QUnit.test( - 'Handling a URL on a route marked as inaccessible behaves like a failed url match', - function (assert) { - assert.expect(1); - - routes = { - admin: createHandler('admin', { - inaccessibleByURL: true, - }), - }; - - router - .handleURL('/index') - .then(function () { - return router.handleURL('/admin/posts'); - }) - .then(shouldNotHappen(assert), function (e: Error) { - assert.equal(e.name, 'UnrecognizedURLError', 'error.name is UnrecognizedURLError'); - }); - } - ); - - QUnit.module('Intermediate transitions (' + scenario.name + ')', { - beforeEach: function (assert: Assert) { - routes = {}; - expectedUrl = null; - - map(assert, function (match) { - match('/').to('application', function (match) { - //match("/").to("index"); - match('/foo').to('foo'); - match('/loading').to('loading'); - }); - }); - }, - }); - - QUnit.test( - 'intermediateTransitionTo() has the correct RouteInfo objects', - async function (assert) { - assert.expect(9); - routes = { - application: createHandler('application'), - foo: createHandler('foo', { - model: function () { - router.intermediateTransitionTo('loading'); - return new Promise(function (resolve) { - resolve(); - }); - }, - }), - loading: createHandler('loading'), - }; - - let enteredCount = 0; - router.routeWillChange = (transition: Transition) => { - if (enteredCount === 0) { - assert.equal(transition.to!.name, 'foo', 'going to'); - assert.equal(transition.to!.queryParams['qux'], '42', 'going to with query params'); - } else if (enteredCount === 1) { - assert.equal(transition.to!.name, 'loading', 'entering'); - assert.equal( - transition.to!.queryParams['qux'], - '42', - 'intermediate also has query params' - ); - // https://github.com/emberjs/ember.js/issues/14438 - assert.equal(transition[STATE_SYMBOL].routeInfos.length, 2, 'with routeInfos present'); - } - enteredCount++; - assert.equal(transition.from, null); - }; - - router.routeDidChange = (transition: Transition) => { - assert.equal(transition.to!.name, 'foo', 'landed at'); - assert.equal(enteredCount, 2); - }; - - await router.transitionTo('/foo?qux=42'); - } - ); - - QUnit.test( - "intermediateTransitionTo() forces an immediate intermediate transition that doesn't cancel currently active async transitions", - async function (assert) { - assert.expect(11); - - let counter = 1, - willResolves: Route[], - appModel = {}, - fooModel = {}; - - function counterAt(expectedValue: number, description: string) { - assert.equal(counter, expectedValue, 'Step ' + expectedValue + ': ' + description); - counter++; - } - - routes = { - application: createHandler('application', { - model: function () { - return appModel; - }, - setup: function (obj: Dict) { - counterAt(1, 'application#setup'); - assert.equal(obj, appModel, 'application#setup is passed the return value from model'); - }, - events: { - willResolveModel: function (_transition: Transition, handler: Route) { - assert.equal( - willResolves.shift(), - handler, - 'willResolveModel event fired and passed expanded handler' - ); - }, - }, - }), - foo: createHandler('foo', { - model: function () { - router.intermediateTransitionTo('loading'); - counterAt(3, 'intermediate transition finished within foo#model'); - - return new Promise(function (resolve) { - counterAt(4, "foo's model promise resolves"); - resolve(fooModel); - }); - }, - setup: function (obj: Dict) { - counterAt(6, 'foo#setup'); - assert.equal(obj, fooModel, 'foo#setup is passed the resolve model promise'); - }, - }), - loading: createHandler('loading', { - model: function () { - assert.ok(false, "intermediate transitions don't call model hooks"); - }, - setup: function () { - counterAt(2, 'loading#setup'); - }, - exit: function () { - counterAt(5, 'loading state exited'); - }, - }), - }; - - willResolves = [routes['application']!, routes['foo']!]; - - await router.transitionTo('/foo'); - - counterAt(7, 'original transition promise resolves'); - } - ); - - QUnit.test( - 'Calling transitionTo during initial transition in validation hook should use replaceURL', - async function (assert) { - assert.expect(4); - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - }); - - let fooModelCount = 0, - barModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(false, 'The url was not correctly replaced on initial transition'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(true, 'The url was replaced correctly on initial transition'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.transitionTo('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - }; - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/bar'); - assert.equal(fooModelCount, 1); - assert.equal(barModelCount, 1); - } - ); - - QUnit.test( - 'Calling transitionTo during initial transition in validation hook with multiple redirects should use replaceURL', - async function (assert) { - assert.expect(5); - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(false, 'The url was not correctly replaced on initial transition'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(true, 'The url was replaced correctly on initial transition'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.transitionTo('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.transitionTo('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - }; - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/baz'); - assert.equal(fooModelCount, 1); - assert.equal(barModelCount, 1); - assert.equal(bazModelCount, 1); - } - ); - - QUnit.test( - 'Calling transitionTo after initial transition in validation hook should use updateUrl', - async function (assert) { - assert.expect(8); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - }); - - let fooModelCount = 0, - barModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(true, 'updateURL should be used'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(false, 'replaceURL should not be used'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.transitionTo('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - }; - - await router.transitionTo('/bar'); - - assert.equal(url, '/bar'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - assert.equal(fooModelCount, 0, 'Foo model should not be called'); - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/bar'); - assert.equal(barModelCount, 2, 'Bar model should be called twice'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - } - ); - - QUnit.test( - 'Calling transitionTo after initial transition in validation hook with multiple redirects should use updateUrl', - async function (assert) { - assert.expect(10); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(true, 'updateURL should be used'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(false, 'replaceURL should not be used'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.transitionTo('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.transitionTo('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - }; - - await router.transitionTo('/baz'); - - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 1, 'Baz model should be called once'); - assert.equal(fooModelCount, 0, 'Foo model should not be called'); - assert.equal(barModelCount, 0, 'Bar model should not be called'); - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 2, 'Baz model should be called twice'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - } - ); - - QUnit.test( - 'Calling replaceWith during initial transition in validation hook should use replaceURL', - async function (assert) { - assert.expect(4); - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - }); - - let fooModelCount = 0, - barModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(false, 'The url was not correctly replaced on initial transition'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(true, 'The url was replaced correctly on initial transition'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - }; - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/bar'); - assert.equal(fooModelCount, 1); - assert.equal(barModelCount, 1); - } - ); - - QUnit.test( - 'Calling replaceWith during initial transition in validation hook with multiple redirects should use replaceURL', - async function (assert) { - assert.expect(5); - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(false, 'The url was not correctly replaced on initial transition'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(true, 'The url was replaced correctly on initial transition'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.replaceWith('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - }; - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/baz'); - assert.equal(fooModelCount, 1, 'should call foo model once'); - assert.equal(barModelCount, 1, 'should call bar model once'); - assert.equal(bazModelCount, 1, 'should call baz model once'); - } - ); - - QUnit.test( - 'Calling replaceWith after initial transition in validation hook should use updateUrl', - async function (assert) { - assert.expect(8); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - }); - - let fooModelCount = 0, - barModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(true, 'updateURL should be used'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(false, 'replaceURL should not be used'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - }; - - await router.transitionTo('/bar'); - - assert.equal(url, '/bar'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - assert.equal(fooModelCount, 0, 'Foo model should not be called'); - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/bar'); - assert.equal(barModelCount, 2, 'Bar model should be called twice'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - } - ); - - QUnit.test( - 'Calling replaceWith after initial transition in validation hook with multiple redirects should use updateUrl', - async function (assert) { - assert.expect(10); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(true, 'updateURL should be used'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(false, 'replaceURL should not be used'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.replaceWith('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - }; - - await router.transitionTo('/baz'); - - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 1, 'Bar model should be called once'); - assert.equal(fooModelCount, 0, 'Foo model should not be called'); - assert.equal(barModelCount, 0, 'Baz model should not be called'); - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 2, 'Baz model should be called twice'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - } - ); - - QUnit.test( - 'Calling replaceWith after initial replace in validation hook with multiple redirects should use replaceUrl', - async function (assert) { - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - match('/qux').to('qux'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0, - history: string[] = []; - - router.updateURL = function (updateUrl) { - url = updateUrl; - history.push(url); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - if (history.length === 0) { - assert.ok(false, 'should not replace on initial'); - } - history[history.length - 1] = url; - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.replaceWith('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - let quxHandler = createHandler('qux', { - model: function () {}, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - qux: quxHandler, - }; - - await router.transitionTo('/qux'); - - assert.equal(history.length, 1, 'only one history item'); - assert.equal(history[0], '/qux', 'history item is /qux'); - - await ignoreTransitionError(replaceWith(router, '/foo')); - - assert.equal(history.length, 1, 'still only one history item, replaced the previous'); - assert.equal(history[0], '/baz', 'history item is /foo'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - assert.equal(bazModelCount, 1, 'Baz model should be called once'); - } - ); - - QUnit.test( - 'Mixing multiple types of redirect during initial transition should work', - async function (assert) { - assert.expect(10); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(true, 'updateURL should be used'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(false, 'replaceURL should not be used'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.transitionTo('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - }; - - await router.transitionTo('/baz'); - - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 1, 'Bar model should be called once'); - assert.equal(fooModelCount, 0, 'Foo model should not be called'); - assert.equal(barModelCount, 0, 'Baz model should not be called'); - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 2, 'Baz model should be called twice'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - } - ); - - QUnit.test( - 'Mixing multiple types of redirects after initial transition should work', - async function (assert) { - assert.expect(12); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - match('/baz').to('baz'); - }); - - let fooModelCount = 0, - barModelCount = 0, - bazModelCount = 0, - updateUrlCount = 0, - replaceUrlCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - updateUrlCount++; - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - replaceUrlCount++; - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - router.replaceWith('/bar'); - }, - }); - - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - router.transitionTo('/baz'); - }, - }); - - let bazHandler = createHandler('baz', { - model: function () { - bazModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - baz: bazHandler, - }; - - await router.transitionTo('/baz'); - // actually replaceURL probably makes more sense here, but it's an initial - // transition to a route that the page loaded on, so it's a no-op and doesn't - // cause a problem - assert.equal(replaceUrlCount, 0, 'replaceURL should not be used'); - assert.equal(updateUrlCount, 1, 'updateURL should be used for initial transition'); - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 1, 'Baz model should be called once'); - assert.equal(fooModelCount, 0, 'Foo model should not be called'); - assert.equal(barModelCount, 0, 'Bar model should not be called'); - - await ignoreTransitionError(router.transitionTo('/foo')); - - assert.equal(replaceUrlCount, 0, 'replaceURL should not be used'); - assert.equal(updateUrlCount, 2, 'updateURL should be used for subsequent transition'); - assert.equal(url, '/baz'); - assert.equal(bazModelCount, 2, 'Baz model should be called twice'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - } - ); - - QUnit.test( - 'Calling replaceWith after initial transition outside validation hook should use replaceURL', - async function (assert) { - assert.expect(7); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - }); - - let fooModelCount = 0, - barModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.equal(updateUrl, '/foo', 'incorrect url for updateURL'); - }; - - router.replaceURL = function (replaceUrl) { - url = replaceUrl; - assert.equal(replaceUrl, '/bar', 'incorrect url for replaceURL'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - }, - }); - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - }; - - await router.transitionTo('/foo'); - - assert.equal(url, '/foo', 'failed initial transition'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 0, 'Bar model should not be called'); - - await router.replaceWith('/bar'); - - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - } - ); - - QUnit.test( - 'Calling transitionTo after initial transition outside validation hook should use updateUrl', - async function (assert) { - assert.expect(7); - - map(assert, function (match) { - match('/foo').to('foo'); - match('/bar').to('bar'); - }); - - let fooModelCount = 0, - barModelCount = 0; - - router.updateURL = function (updateUrl) { - url = updateUrl; - assert.ok(true, 'updateURL is used'); - }; - - router.replaceURL = function (replaceURL) { - url = replaceURL; - assert.ok(false, 'replaceURL should not be used'); - }; - - let fooHandler = createHandler('foo', { - model: function () { - fooModelCount++; - }, - }); - let barHandler = createHandler('bar', { - model: function () { - barModelCount++; - }, - }); - - routes = { - foo: fooHandler, - bar: barHandler, - }; - - await router.transitionTo('/foo'); - - assert.equal(url, '/foo', 'failed initial transition'); - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 0, 'Bar model should not be called'); - - await router.transitionTo('/bar'); - - assert.equal(fooModelCount, 1, 'Foo model should be called once'); - assert.equal(barModelCount, 1, 'Bar model should be called once'); - } - ); - - QUnit.test( - 'transitioning to the same route with different context should not reenter the route', - async function (assert) { - map(assert, function (match) { - match('/project/:project_id').to('project'); - }); - - let projectEnterCount = 0; - let projectSetupCount = 0; - let projectHandler = createHandler('project', { - model: function (params: Dict) { - delete params['queryParams']; - return params; - }, - enter: function () { - projectEnterCount++; - }, - setup: function () { - projectSetupCount++; - }, - }); - - routes = { - project: projectHandler, - }; - - await router.transitionTo('/project/1'); - assert.equal(projectEnterCount, 1, 'project handler should have been entered once'); - assert.equal(projectSetupCount, 1, 'project handler should have been setup once'); - - await router.transitionTo('/project/2'); - assert.equal( - projectEnterCount, - 1, - 'project handler should still have been entered only once' - ); - assert.equal(projectSetupCount, 2, 'project handler should have been setup twice'); - } - ); - - QUnit.test( - 'synchronous transition errors can be detected synchronously', - async function (assert) { - map(assert, function (match) { - match('/').to('root'); - }); - - router.getRoute = function () { - throw new Error('boom!'); - }; - - const transition = router.transitionTo('/'); - - assert.rejects(transition as unknown as Promise); - assert.equal((transition.error as Error).message, 'boom!'); - } - ); -}); diff --git a/packages/router_js/tests/test_helpers.ts b/packages/router_js/tests/test_helpers.ts deleted file mode 100644 index dcba75cd40e..00000000000 --- a/packages/router_js/tests/test_helpers.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { Route, Transition } from '../index'; -import Router from '../index'; -import type { Dict } from '../lib/core'; -import type { IModel } from '../lib/route-info'; -import RouteInfo, { UnresolvedRouteInfoByParam } from '../lib/route-info'; -import type { PublicTransition } from '../lib/transition'; -import { logAbort } from '../lib/transition'; -import type { TransitionError } from '../lib/transition-state'; -import type { UnrecognizedURLError } from '../lib/unrecognized-url-error'; -import { isTransitionAborted } from '../lib/transition-aborted-error'; - -QUnit.config.testTimeout = 1000; - -// A useful function to allow you to ignore transition errors in a testing context -export async function ignoreTransitionError(transition: Transition) { - try { - await transition; - } catch { - // if it errors we don't do anything - } -} - -function assertAbort(assert: Assert) { - return function _assertAbort(e: Error) { - assert.ok(isTransitionAborted(e), 'transition was redirected/aborted'); - }; -} - -function transitionToWithAbort(assert: Assert, router: Router, path: string) { - return router.transitionTo(path).then(shouldNotHappen(assert), assertAbort(assert)); -} - -function replaceWith(router: Router, path: string) { - return router.transitionTo.apply(router, [path]).method('replace'); -} - -function shouldNotHappen(assert: Assert, _message?: string) { - let message = _message || 'this .then handler should not be called'; - return function _shouldNotHappen(error: any) { - console.error(error.stack); // eslint-disable-line - assert.ok(false, message); - return error; - }; -} - -export function isExiting(route: Route | string, routeInfos: RouteInfo[]) { - for (let i = 0, len = routeInfos.length; i < len; ++i) { - let routeInfo = routeInfos[i]; - if (routeInfo!.name === route || routeInfo!.route === route) { - return false; - } - } - return true; -} - -function stubbedHandlerInfoFactory(name: string, props: Dict) { - let obj = Object.create(props); - obj._handlerInfoType = name; - return obj; -} - -export { - transitionToWithAbort, - replaceWith, - shouldNotHappen, - stubbedHandlerInfoFactory, - assertAbort, -}; - -export function createHandler(name: string, options?: Dict): Route { - return Object.assign( - { name, routeName: name, context: {}, names: [], handler: name, _internalName: name }, - options - ) as unknown as Route; -} - -export class TestRouter extends Router { - didTransition() {} - willTransition() {} - updateURL(_url: string): void {} - replaceURL(_url: string): void {} - triggerEvent( - _handlerInfos: RouteInfo[], - _ignoreFailure: boolean, - _name: string, - _args: any[] - ) {} - routeDidChange() {} - routeWillChange() {} - transitionDidError(error: TransitionError, transition: PublicTransition) { - if (error.wasAborted || transition.isAborted) { - return logAbort(transition); - } else { - transition.trigger(false, 'error', error.error, this, error.route); - transition.abort(); - return error.error; - } - } - getRoute(_name: string): any { - return {}; - } - getSerializer(_name: string): any { - return () => {}; - } -} - -export function createHandlerInfo(name: string, options: Dict = {}): RouteInfo { - class Stub extends RouteInfo { - constructor(name: string, router: Router, handler?: Route) { - super(router, name, [], handler); - } - getModel(_transition: Transition) { - return {} as any; - } - getUnresolved() { - return new UnresolvedRouteInfoByParam(this.router, 'empty', [], {}); - } - } - - let handler = (options['handler'] as Route) || createHandler('foo'); - delete options['handler']; - - Object.assign(Stub.prototype, options); - let stub = new Stub(name, new TestRouter(), handler); - return stub; -} - -export function trigger( - handlerInfos: RouteInfo[], - ignoreFailure: boolean, - name: string, - ...args: any[] -) { - if (!handlerInfos) { - if (ignoreFailure) { - return; - } - throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); - } - - let eventWasHandled = false; - - for (let i = handlerInfos.length - 1; i >= 0; i--) { - let currentHandlerInfo = handlerInfos[i]!, - currentHandler = currentHandlerInfo.route; - - // If there is no handler, it means the handler hasn't resolved yet which - // means that we should trigger the event later when the handler is available - if (!currentHandler) { - currentHandlerInfo.routePromise!.then(function (resolvedHandler) { - if (resolvedHandler.events?.[name]) { - resolvedHandler.events[name].apply(resolvedHandler, args); - } - }); - continue; - } - - if (currentHandler.events && currentHandler.events[name]) { - if (currentHandler.events[name].apply(currentHandler, args) === true) { - eventWasHandled = true; - } else { - return; - } - } - } - - // In the case that we got an UnrecognizedURLError as an event with no handler, - // let it bubble up - if (name === 'error' && (args[0] as UnrecognizedURLError)!.name === 'UnrecognizedURLError') { - throw args[0]; - } else if (!eventWasHandled && !ignoreFailure) { - throw new Error("Nothing handled the event '" + name + "'."); - } -} diff --git a/packages/router_js/tests/transition-aborted-error_test.ts b/packages/router_js/tests/transition-aborted-error_test.ts deleted file mode 100644 index 14267e5456c..00000000000 --- a/packages/router_js/tests/transition-aborted-error_test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - throwIfAborted, - isTransitionAborted, - buildTransitionAborted, -} from '../lib/transition-aborted-error'; - -QUnit.module('transition-aborted-error'); - -QUnit.test('correct inheritance and name', function (assert) { - let error; - - try { - throw buildTransitionAborted(); - } catch (e) { - error = e; - } - - // it would be more correct with TransitionAbortedError, but other libraries may rely on this name - assert.equal( - (error as Error).name, - 'TransitionAborted', - "TransitionAbortedError has the name 'TransitionAborted'" - ); - - assert.ok(isTransitionAborted(error)); - assert.ok(error instanceof Error); -}); - -QUnit.test('throwIfAborted', function (assert) { - throwIfAborted(undefined); - throwIfAborted(null); - throwIfAborted({}); - throwIfAborted({ apple: false }); - throwIfAborted({ isAborted: false }); - throwIfAborted({ isAborted: false, other: 'key' }); - assert.throws(() => throwIfAborted({ isAborted: true }), /TransitionAborted/); - assert.throws(() => throwIfAborted({ isAborted: true, other: 'key' }), /TransitionAborted/); -}); diff --git a/packages/router_js/tests/transition_intent_test.ts b/packages/router_js/tests/transition_intent_test.ts deleted file mode 100644 index 028c1e70001..00000000000 --- a/packages/router_js/tests/transition_intent_test.ts +++ /dev/null @@ -1,306 +0,0 @@ -import NamedTransitionIntent from '../lib/transition-intent/named-transition-intent'; -import URLTransitionIntent from '../lib/transition-intent/url-transition-intent'; -import TransitionState from '../lib/transition-state'; -import { createHandler, TestRouter } from './test_helpers'; - -import type { default as Router, Route } from '../index'; -import type { Dict } from '../lib/core'; -import { - type default as InternalRouteInfo, - ResolvedRouteInfo, - UnresolvedRouteInfoByObject, - UnresolvedRouteInfoByParam, -} from '../lib/route-info'; -import { Promise } from 'rsvp'; - -let handlers: Dict, recognizer: any; - -let scenarios = [ - { - name: 'Sync Get Handler', - async: false, - getHandler: function (name: string) { - return handlers[name] || (handlers[name] = createHandler(name)); - }, - }, - { - name: 'Async Get Handler', - async: true, - getHandler: function (name: string) { - return Promise.resolve(handlers[name] || (handlers[name] = createHandler(name))); - }, - }, -]; - -scenarios.forEach(function (scenario) { - class TransitionRouter extends TestRouter { - getRoute(name: string) { - return scenario.getHandler(name); - } - } - - let router: Router; - - // Asserts that a handler from a handlerInfo equals an expected valued. - // Returns a promise during async scenarios to wait until the handler is ready. - function assertHandlerEquals( - assert: Assert, - handlerInfo: InternalRouteInfo, - expected: Route - ) { - if (!scenario.async) { - return assert.equal(handlerInfo.route, expected); - } else { - assert.equal(handlerInfo.route, undefined); - return handlerInfo.routePromise.then(function (handler) { - assert.equal(handler, expected); - }); - } - } - - // TODO: remove repetition, DRY in to test_helpers. - QUnit.module('TransitionIntent (' + scenario.name + ')', { - beforeEach: function () { - handlers = {}; - - handlers['foo'] = createHandler('foo'); - handlers['bar'] = createHandler('bar'); - handlers['articles'] = createHandler('articles'); - handlers['comments'] = createHandler('comments'); - - recognizer = { - handlersFor: function (name: string) { - if (name === 'comments') { - return [ - { - handler: 'articles', - names: ['article_id'], - }, - { - handler: 'comments', - names: ['comment_id'], - }, - ]; - } - return; - }, - hasRoute: function (name: string) { - return name === 'comments'; - }, - recognize: function (url: string) { - if (url === '/foo/bar') { - return [ - { - handler: 'foo', - isDynamic: false, - params: {}, - }, - { - handler: 'bar', - isDynamic: false, - params: {}, - }, - ]; - } else if (url === '/articles/123/comments/456') { - return [ - { - handler: 'articles', - isDynamic: true, - params: { article_id: '123' }, - }, - { - handler: 'comments', - isDynamic: true, - params: { comment_id: '456' }, - }, - ]; - } - - return; - }, - }; - - router = new TransitionRouter(); - router.recognizer = recognizer; - }, - }); - - QUnit.test('URLTransitionIntent can be applied to an empty state', function (assert) { - let state = new TransitionState(); - let intent = new URLTransitionIntent(router, '/foo/bar'); - let newState = intent.applyToState(state); - let handlerInfos = newState.routeInfos; - - assert.equal(handlerInfos.length, 2); - assert.notOk( - handlerInfos[0]!.isResolved, - 'generated state consists of unresolved handler info, 1' - ); - assert.notOk( - handlerInfos[1]!.isResolved, - 'generated state consists of unresolved handler info, 2' - ); - Promise.all([ - assertHandlerEquals(assert, handlerInfos[0]!, handlers['foo']!), - assertHandlerEquals(assert, handlerInfos[1]!, handlers['bar']!), - ]); - }); - - QUnit.test('URLTransitionIntent applied to single unresolved URL handlerInfo', function (assert) { - let state = new TransitionState(); - - let startingHandlerInfo = new UnresolvedRouteInfoByParam( - router, - 'foo', - [], - {}, - handlers['foo'] - ); - - // This single unresolved handler info will be preserved - // in the new array of handlerInfos. - // Reason: if it were resolved, we wouldn't want to replace it. - // So we only want to replace if it's actually known to be - // different. - state.routeInfos = [startingHandlerInfo]; - - let intent = new URLTransitionIntent(router, '/foo/bar'); - let newState = intent.applyToState(state); - let handlerInfos = newState.routeInfos; - - assert.equal(handlerInfos.length, 2); - assert.equal( - handlerInfos[0], - startingHandlerInfo, - "The starting foo handlerInfo wasn't overridden because the new one wasn't any different" - ); - assert.ok( - handlerInfos[1] instanceof UnresolvedRouteInfoByParam, - 'generated state consists of UnresolvedHandlerInfoByParam, 2' - ); - assertHandlerEquals(assert, handlerInfos[1]!, handlers['bar']!); - }); - - QUnit.test('URLTransitionIntent applied to an already-resolved handlerInfo', function (assert) { - let state = new TransitionState(); - - let startingHandlerInfo = new ResolvedRouteInfo(router, 'foo', [], {}, handlers['foo']!); - - state.routeInfos = [startingHandlerInfo]; - - let intent = new URLTransitionIntent(router, '/foo/bar'); - let newState = intent.applyToState(state); - let handlerInfos = newState.routeInfos; - - assert.equal(handlerInfos.length, 2); - assert.equal( - handlerInfos[0], - startingHandlerInfo, - "The starting foo resolved handlerInfo wasn't overridden because the new one wasn't any different" - ); - assert.ok( - handlerInfos[1] instanceof UnresolvedRouteInfoByParam, - 'generated state consists of UnresolvedHandlerInfoByParam, 2' - ); - assertHandlerEquals(assert, handlerInfos[1]!, handlers['bar']!); - }); - - QUnit.test( - 'URLTransitionIntent applied to an already-resolved handlerInfo (non-empty params)', - function (assert) { - let state = new TransitionState(); - let article = {}; - - let startingHandlerInfo = new ResolvedRouteInfo( - router, - 'articles', - [], - { article_id: 'some-other-id' }, - createHandler('articles'), - article - ); - - state.routeInfos = [startingHandlerInfo]; - - let intent = new URLTransitionIntent(router, '/articles/123/comments/456'); - let newState = intent.applyToState(state); - let handlerInfos = newState.routeInfos; - - assert.equal(handlerInfos.length, 2); - assert.notStrictEqual( - handlerInfos[0], - startingHandlerInfo, - 'The starting foo resolved handlerInfo was overridden because the new had different params' - ); - assert.ok( - handlerInfos[1] instanceof UnresolvedRouteInfoByParam, - 'generated state consists of UnresolvedHandlerInfoByParam, 2' - ); - - assertHandlerEquals(assert, handlerInfos[1]!, handlers['comments']!); - } - ); - - QUnit.test( - 'URLTransitionIntent applied to an already-resolved handlerInfo of different route', - function (assert) { - let state = new TransitionState(); - - let startingHandlerInfo = new ResolvedRouteInfo(router, 'alex', [], {}, handlers['foo']!); - - state.routeInfos = [startingHandlerInfo]; - - let intent = new URLTransitionIntent(router, '/foo/bar'); - let newState = intent.applyToState(state); - let handlerInfos = newState.routeInfos; - - assert.equal(handlerInfos.length, 2); - assert.notStrictEqual( - handlerInfos[0], - startingHandlerInfo, - 'The starting foo resolved handlerInfo gets overridden because the new one has a different name' - ); - assert.ok( - handlerInfos[1] instanceof UnresolvedRouteInfoByParam, - 'generated state consists of UnresolvedHandlerInfoByParam, 2' - ); - assertHandlerEquals(assert, handlerInfos[1]!, handlers['bar']!); - } - ); - - QUnit.test( - 'NamedTransitionIntent applied to an already-resolved handlerInfo (non-empty params)', - function (assert) { - let state = new TransitionState(); - - let article = {}; - let comment = {}; - - let startingHandlerInfo = new ResolvedRouteInfo( - router, - 'articles', - [], - { article_id: 'some-other-id' }, - createHandler('articles'), - article - ); - - state.routeInfos = [startingHandlerInfo]; - - let intent = new NamedTransitionIntent(router, 'comments', undefined, [article, comment]); - - let newState = intent.applyToState(state, false); - let handlerInfos = newState.routeInfos; - - assert.equal(handlerInfos.length, 2); - assert.equal(handlerInfos[0], startingHandlerInfo); - assert.equal(handlerInfos[0]!.context, article); - assert.ok( - handlerInfos[1] instanceof UnresolvedRouteInfoByObject, - 'generated state consists of UnresolvedHandlerInfoByObject, 2' - ); - assert.equal(handlerInfos[1]!.context, comment); - assertHandlerEquals(assert, handlerInfos[1]!, handlers['comments']!); - } - ); -}); diff --git a/packages/router_js/tests/transition_state_test.ts b/packages/router_js/tests/transition_state_test.ts deleted file mode 100644 index 099d3e0f40a..00000000000 --- a/packages/router_js/tests/transition_state_test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Transition } from '../index'; -import type { Dict } from '../lib/core'; -import { - type Route, - UnresolvedRouteInfoByObject, - UnresolvedRouteInfoByParam, -} from '../lib/route-info'; -import TransitionState, { type TransitionError } from '../lib/transition-state'; -import { Promise, resolve } from 'rsvp'; -import { createHandler, createHandlerInfo, TestRouter } from './test_helpers'; - -QUnit.module('TransitionState'); - -QUnit.test('it starts off with default state', function (assert) { - let state = new TransitionState(); - assert.deepEqual(state.routeInfos, [], 'it has an array of handlerInfos'); -}); - -QUnit.test("#resolve delegates to handleInfo objects' resolve()", function (assert) { - assert.expect(3); - - let state = new TransitionState(); - - let counter = 0; - - let resolvedHandlerInfos: any[] = [{}, {}]; - - state.routeInfos = [ - createHandlerInfo('one', { - resolve: function () { - ++counter; - assert.equal(counter, 1); - return resolve(resolvedHandlerInfos[0]); - }, - }), - createHandlerInfo('two', { - resolve: function () { - ++counter; - assert.equal(counter, 2); - return resolve(resolvedHandlerInfos[1]); - }, - }), - ]; - - state.resolve({} as Transition).then(function (result: TransitionState) { - assert.deepEqual(result.routeInfos, resolvedHandlerInfos); - }); -}); - -QUnit.test('State resolution can be halted', async function (assert) { - assert.expect(1); - - let state = new TransitionState(); - - state.routeInfos = [ - createHandlerInfo('one', { - resolve: function () {}, - }), - createHandlerInfo('two', { - resolve: function () { - assert.ok(false, 'I should not be entered because we threw an error in shouldContinue'); - }, - }), - ]; - - let fakeTransition = {} as Transition; - fakeTransition.isAborted = true; - - await state.resolve(fakeTransition).catch(function (reason: TransitionError) { - assert.ok(reason.wasAborted, 'state resolution was correctly marked as aborted'); - }); -}); - -QUnit.test('Integration w/ HandlerInfos', function (assert) { - assert.expect(4); - - let state = new TransitionState(); - let router = new TestRouter(); - let fooModel = {}; - let barModel = {}; - let transition = {}; - - state.routeInfos = [ - new UnresolvedRouteInfoByParam( - router, - 'foo', - ['foo_id'], - { foo_id: '123' }, - createHandler('foo', { - model: function (params: Dict, payload: Dict) { - assert.equal(payload, transition); - assert.equal(params['foo_id'], '123', 'foo#model received expected params'); - return resolve(fooModel); - }, - }) - ), - new UnresolvedRouteInfoByObject(router, 'bar', ['bar_id'], resolve(barModel)), - ]; - - state - .resolve(transition as Transition) - .then(function (result: TransitionState) { - let models = []; - for (let i = 0; i < result.routeInfos.length; i++) { - models.push(result.routeInfos[i]!.context); - } - - assert.equal(models[0], fooModel); - assert.equal(models[1], barModel); - return Promise.resolve(new TransitionState()); - }) - .catch(function (error: Error) { - assert.ok(false, 'Caught error: ' + error); - }); -}); diff --git a/packages/router_js/tests/unrecognized-url-error_test.ts b/packages/router_js/tests/unrecognized-url-error_test.ts deleted file mode 100644 index 5730e6b5108..00000000000 --- a/packages/router_js/tests/unrecognized-url-error_test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import UnrecognizedURLError from '../lib/unrecognized-url-error'; - -QUnit.module('unrecognized-url-error'); - -QUnit.test('correct inheritance', function (assert) { - let error; - - try { - throw new UnrecognizedURLError('Message'); - } catch (e) { - error = e; - } - - assert.ok(error instanceof UnrecognizedURLError); - assert.ok(error instanceof Error); -}); diff --git a/packages/router_js/tests/utils_test.ts b/packages/router_js/tests/utils_test.ts deleted file mode 100644 index 06ba8270b7a..00000000000 --- a/packages/router_js/tests/utils_test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { getChangelist } from '../lib/utils'; - -QUnit.module('utils'); - -QUnit.test('getChangelist', function (assert) { - let result = getChangelist({}, { foo: '123' }); - assert.deepEqual(result, { - all: { foo: '123' }, - changed: { foo: '123' }, - removed: {}, - }); - - result = getChangelist({ foo: '123' }, { foo: '123' }); - assert.notOk(result); - - result = getChangelist({ foo: '123' }, {}); - assert.deepEqual(result, { all: {}, changed: {}, removed: { foo: '123' } }); - - result = getChangelist({ foo: '123', bar: '456' }, { foo: '123' }); - assert.deepEqual(result, { - all: { foo: '123' }, - changed: {}, - removed: { bar: '456' }, - }); - - result = getChangelist({ foo: '123', bar: '456' }, { foo: '456' }); - assert.deepEqual(result, { - all: { foo: '456' }, - changed: { foo: '456' }, - removed: { bar: '456' }, - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6df73016386..aa0aee92b76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: route-recognizer: specifier: ^0.3.4 version: 0.3.4 + router_js: + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) semver: specifier: ^7.5.2 version: 7.7.4 @@ -226,9 +229,6 @@ importers: rollup: specifier: ^4.2.0 version: 4.59.0 - router_js: - specifier: workspace:* - version: link:packages/router_js rsvp: specifier: ^4.8.5 version: 4.8.5 @@ -395,8 +395,8 @@ importers: specifier: workspace:* version: link:../../internal-test-helpers router_js: - specifier: workspace:* - version: link:../../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) rsvp: specifier: ^4.8.5 version: 4.8.5 @@ -477,8 +477,8 @@ importers: specifier: workspace:* version: link:../../internal-test-helpers router_js: - specifier: workspace:* - version: link:../../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) packages/@ember/array: dependencies: @@ -742,8 +742,8 @@ importers: specifier: workspace:* version: link:../../internal-test-helpers router_js: - specifier: workspace:* - version: link:../../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) packages/@ember/enumerable: dependencies: @@ -1025,8 +1025,8 @@ importers: specifier: workspace:* version: link:../../internal-test-helpers router_js: - specifier: workspace:* - version: link:../../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) packages/@ember/runloop: dependencies: @@ -2451,8 +2451,8 @@ importers: specifier: workspace:* version: link:../internal-test-helpers router_js: - specifier: workspace:* - version: link:../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) rsvp: specifier: ^4.8.5 version: 4.8.5 @@ -2610,8 +2610,8 @@ importers: specifier: workspace:* version: link:../internal-test-helpers router_js: - specifier: workspace:* - version: link:../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) packages/internal-test-helpers: dependencies: @@ -2712,8 +2712,8 @@ importers: specifier: workspace:* version: link:../ember-template-compiler router_js: - specifier: workspace:* - version: link:../router_js + specifier: ^8.0.6 + version: 8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5) rsvp: specifier: ^4.8.5 version: 4.8.5 @@ -2723,34 +2723,6 @@ importers: packages/loader: {} - packages/router_js: - dependencies: - '@glimmer/env': - specifier: workspace:* - version: link:../@glimmer/env - devDependencies: - '@types/qunit': - specifier: ^2.9.6 - version: 2.19.13 - '@types/rsvp': - specifier: ^4.0.4 - version: 4.0.9 - backburner.js: - specifier: ^2.6.0 - version: 2.8.0 - loader.js: - specifier: ^4.7.0 - version: 4.7.0 - qunit: - specifier: ^2.11.3 - version: 2.25.0 - route-recognizer: - specifier: ^0.3.4 - version: 0.3.4 - rsvp: - specifier: ^4.8.5 - version: 4.8.5 - smoke-tests/app-template: devDependencies: '@babel/core': @@ -10949,6 +10921,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qified@0.6.0: @@ -11289,6 +11262,13 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + router_js@8.0.6: + resolution: {integrity: sha512-AjGxRDIpTGoAG8admFmvP/cxn1AlwwuosCclMU4R5oGHGt7ER0XtB3l9O04ToBDdPe4ivM/YcLopgBEpJssJ/Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + route-recognizer: ^0.3.4 + rsvp: ^4.8.5 + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -16679,7 +16659,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/chai-as-promised@7.1.8': dependencies: @@ -16693,7 +16673,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/cors@2.8.19': dependencies: @@ -16720,7 +16700,7 @@ snapshots: '@types/express-serve-static-core@4.19.8': dependencies: - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/qs': 6.15.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -16738,7 +16718,7 @@ snapshots: '@types/fs-extra@9.0.13': dependencies: - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/glob@9.0.0': dependencies: @@ -16790,28 +16770,28 @@ snapshots: '@types/rimraf@3.0.2': dependencies: '@types/glob': 9.0.0 - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/rsvp@4.0.9': {} '@types/send@0.17.6': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/send@1.2.1': dependencies: - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/serve-static@1.15.10': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/send': 0.17.6 '@types/ssri@7.1.5': dependencies: - '@types/node': 22.19.15 + '@types/node': 20.19.37 '@types/supports-color@8.1.3': {} @@ -23703,6 +23683,12 @@ snapshots: transitivePeerDependencies: - supports-color + router_js@8.0.6(route-recognizer@0.3.4)(rsvp@4.8.5): + dependencies: + '@glimmer/env': 0.1.7 + route-recognizer: 0.3.4 + rsvp: 4.8.5 + rrweb-cssom@0.7.1: {} rrweb-cssom@0.8.0: {} diff --git a/rollup.config.mjs b/rollup.config.mjs index 70d3ae07bba..d71353b939c 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -294,7 +294,6 @@ function rolledUpPackages() { '@ember/-internals/metal', '@ember/-internals/utils', '@ember/-internals/container', - 'router_js', ]; } @@ -306,7 +305,7 @@ export function exposedDependencies() { 'backburner.js': require.resolve('backburner.js/dist/es6/backburner.js'), rsvp: require.resolve('rsvp/lib/rsvp.js'), 'dag-map': require.resolve('dag-map/dag-map.js'), - router_js: require.resolve('router_js'), + router_js: require.resolve('router_js/dist/modules/index.js'), 'route-recognizer': require.resolve('route-recognizer/dist/route-recognizer.es.js'), ...walkGlimmerDeps([ '@glimmer/node',