From 2044d87975e51429610a050c35a78b6cd2aba6f6 Mon Sep 17 00:00:00 2001 From: yzsunlei Date: Tue, 3 Dec 2019 12:46:08 +0800 Subject: [PATCH] Vue-router-v3.0.6 --- 2.Vue-router/flow/declarations.js | 103 ++++++ 2.Vue-router/src/components/link.js | 137 ++++++++ 2.Vue-router/src/components/view.js | 124 +++++++ 2.Vue-router/src/create-matcher.js | 202 +++++++++++ 2.Vue-router/src/create-route-map.js | 171 +++++++++ 2.Vue-router/src/history/abstract.js | 51 +++ 2.Vue-router/src/history/base.js | 331 ++++++++++++++++++ 2.Vue-router/src/history/hash.js | 145 ++++++++ 2.Vue-router/src/history/html5.js | 80 +++++ 2.Vue-router/src/index.js | 248 +++++++++++++ 2.Vue-router/src/install.js | 52 +++ 2.Vue-router/src/util/async.js | 18 + 2.Vue-router/src/util/dom.js | 3 + 2.Vue-router/src/util/location.js | 64 ++++ 2.Vue-router/src/util/misc.js | 6 + 2.Vue-router/src/util/params.js | 35 ++ 2.Vue-router/src/util/path.js | 74 ++++ 2.Vue-router/src/util/push-state.js | 59 ++++ 2.Vue-router/src/util/query.js | 95 +++++ 2.Vue-router/src/util/resolve-components.js | 108 ++++++ 2.Vue-router/src/util/route.js | 132 +++++++ 2.Vue-router/src/util/scroll.js | 130 +++++++ 2.Vue-router/src/util/warn.js | 17 + 2.Vue-router/test/.eslintrc | 5 + 2.Vue-router/test/e2e/.eslintrc | 5 + 2.Vue-router/test/e2e/nightwatch.config.js | 53 +++ 2.Vue-router/test/e2e/runner.js | 34 ++ 2.Vue-router/test/e2e/specs/active-links.js | 54 +++ 2.Vue-router/test/e2e/specs/auth-flow.js | 54 +++ 2.Vue-router/test/e2e/specs/basic.js | 54 +++ 2.Vue-router/test/e2e/specs/data-fetching.js | 29 ++ 2.Vue-router/test/e2e/specs/hash-mode.js | 56 +++ .../test/e2e/specs/hash-scroll-behavior.js | 61 ++++ 2.Vue-router/test/e2e/specs/keepalive-view.js | 34 ++ .../e2e/specs/lazy-loading-before-mount.js | 11 + 2.Vue-router/test/e2e/specs/lazy-loading.js | 45 +++ 2.Vue-router/test/e2e/specs/named-routes.js | 36 ++ 2.Vue-router/test/e2e/specs/named-views.js | 35 ++ .../test/e2e/specs/navigation-guards.js | 135 +++++++ 2.Vue-router/test/e2e/specs/nested-router.js | 28 ++ 2.Vue-router/test/e2e/specs/nested-routes.js | 91 +++++ 2.Vue-router/test/e2e/specs/redirect.js | 133 +++++++ 2.Vue-router/test/e2e/specs/route-alias.js | 88 +++++ 2.Vue-router/test/e2e/specs/route-matching.js | 129 +++++++ 2.Vue-router/test/e2e/specs/route-props.js | 37 ++ .../test/e2e/specs/scroll-behavior.js | 88 +++++ 2.Vue-router/test/e2e/specs/transitions.js | 40 +++ 2.Vue-router/test/unit/jasmine.json | 9 + 2.Vue-router/test/unit/specs/api.spec.js | 282 +++++++++++++++ 2.Vue-router/test/unit/specs/async.spec.js | 17 + .../test/unit/specs/create-map.spec.js | 174 +++++++++ .../test/unit/specs/create-matcher.spec.js | 78 +++++ .../test/unit/specs/custom-query.spec.js | 18 + .../unit/specs/discrete-components.spec.js | 22 ++ .../test/unit/specs/error-handling.spec.js | 55 +++ 2.Vue-router/test/unit/specs/location.spec.js | 133 +++++++ 2.Vue-router/test/unit/specs/node.spec.js | 38 ++ 2.Vue-router/test/unit/specs/path.spec.js | 77 ++++ 2.Vue-router/test/unit/specs/query.spec.js | 61 ++++ 2.Vue-router/test/unit/specs/route.spec.js | 128 +++++++ 60 files changed, 4812 insertions(+) create mode 100644 2.Vue-router/flow/declarations.js create mode 100644 2.Vue-router/src/components/link.js create mode 100644 2.Vue-router/src/components/view.js create mode 100644 2.Vue-router/src/create-matcher.js create mode 100644 2.Vue-router/src/create-route-map.js create mode 100644 2.Vue-router/src/history/abstract.js create mode 100644 2.Vue-router/src/history/base.js create mode 100644 2.Vue-router/src/history/hash.js create mode 100644 2.Vue-router/src/history/html5.js create mode 100644 2.Vue-router/src/index.js create mode 100644 2.Vue-router/src/install.js create mode 100644 2.Vue-router/src/util/async.js create mode 100644 2.Vue-router/src/util/dom.js create mode 100644 2.Vue-router/src/util/location.js create mode 100644 2.Vue-router/src/util/misc.js create mode 100644 2.Vue-router/src/util/params.js create mode 100644 2.Vue-router/src/util/path.js create mode 100644 2.Vue-router/src/util/push-state.js create mode 100644 2.Vue-router/src/util/query.js create mode 100644 2.Vue-router/src/util/resolve-components.js create mode 100644 2.Vue-router/src/util/route.js create mode 100644 2.Vue-router/src/util/scroll.js create mode 100644 2.Vue-router/src/util/warn.js create mode 100644 2.Vue-router/test/.eslintrc create mode 100644 2.Vue-router/test/e2e/.eslintrc create mode 100644 2.Vue-router/test/e2e/nightwatch.config.js create mode 100644 2.Vue-router/test/e2e/runner.js create mode 100644 2.Vue-router/test/e2e/specs/active-links.js create mode 100644 2.Vue-router/test/e2e/specs/auth-flow.js create mode 100644 2.Vue-router/test/e2e/specs/basic.js create mode 100644 2.Vue-router/test/e2e/specs/data-fetching.js create mode 100644 2.Vue-router/test/e2e/specs/hash-mode.js create mode 100644 2.Vue-router/test/e2e/specs/hash-scroll-behavior.js create mode 100644 2.Vue-router/test/e2e/specs/keepalive-view.js create mode 100644 2.Vue-router/test/e2e/specs/lazy-loading-before-mount.js create mode 100644 2.Vue-router/test/e2e/specs/lazy-loading.js create mode 100644 2.Vue-router/test/e2e/specs/named-routes.js create mode 100644 2.Vue-router/test/e2e/specs/named-views.js create mode 100644 2.Vue-router/test/e2e/specs/navigation-guards.js create mode 100644 2.Vue-router/test/e2e/specs/nested-router.js create mode 100644 2.Vue-router/test/e2e/specs/nested-routes.js create mode 100644 2.Vue-router/test/e2e/specs/redirect.js create mode 100644 2.Vue-router/test/e2e/specs/route-alias.js create mode 100644 2.Vue-router/test/e2e/specs/route-matching.js create mode 100644 2.Vue-router/test/e2e/specs/route-props.js create mode 100644 2.Vue-router/test/e2e/specs/scroll-behavior.js create mode 100644 2.Vue-router/test/e2e/specs/transitions.js create mode 100644 2.Vue-router/test/unit/jasmine.json create mode 100644 2.Vue-router/test/unit/specs/api.spec.js create mode 100644 2.Vue-router/test/unit/specs/async.spec.js create mode 100644 2.Vue-router/test/unit/specs/create-map.spec.js create mode 100644 2.Vue-router/test/unit/specs/create-matcher.spec.js create mode 100644 2.Vue-router/test/unit/specs/custom-query.spec.js create mode 100644 2.Vue-router/test/unit/specs/discrete-components.spec.js create mode 100644 2.Vue-router/test/unit/specs/error-handling.spec.js create mode 100644 2.Vue-router/test/unit/specs/location.spec.js create mode 100644 2.Vue-router/test/unit/specs/node.spec.js create mode 100644 2.Vue-router/test/unit/specs/path.spec.js create mode 100644 2.Vue-router/test/unit/specs/query.spec.js create mode 100644 2.Vue-router/test/unit/specs/route.spec.js diff --git a/2.Vue-router/flow/declarations.js b/2.Vue-router/flow/declarations.js new file mode 100644 index 0000000..942aa73 --- /dev/null +++ b/2.Vue-router/flow/declarations.js @@ -0,0 +1,103 @@ +declare var document: Document; + +declare class RouteRegExp extends RegExp { + keys: Array<{ name: string, optional: boolean }>; +} + +declare type PathToRegexpOptions = { + sensitive?: boolean, + strict?: boolean, + end?: boolean +} + +declare module 'path-to-regexp' { + declare module.exports: { + (path: string, keys?: Array, options?: PathToRegexpOptions): RouteRegExp; + compile: (path: string) => (params: Object) => string; + } +} + +declare type Dictionary = { [key: string]: T } + +declare type NavigationGuard = ( + to: Route, + from: Route, + next: (to?: RawLocation | false | Function | void) => void +) => any + +declare type AfterNavigationHook = (to: Route, from: Route) => any + +type Position = { x: number, y: number }; +type PositionResult = Position | { selector: string, offset?: Position } | void; + +declare type RouterOptions = { + routes?: Array; + mode?: string; + fallback?: boolean; + base?: string; + linkActiveClass?: string; + linkExactActiveClass?: string; + parseQuery?: (query: string) => Object; + stringifyQuery?: (query: Object) => string; + scrollBehavior?: ( + to: Route, + from: Route, + savedPosition: ?Position + ) => PositionResult | Promise; +} + +declare type RedirectOption = RawLocation | ((to: Route) => RawLocation) + +declare type RouteConfig = { + path: string; + name?: string; + component?: any; + components?: Dictionary; + redirect?: RedirectOption; + alias?: string | Array; + children?: Array; + beforeEnter?: NavigationGuard; + meta?: any; + props?: boolean | Object | Function; + caseSensitive?: boolean; + pathToRegexpOptions?: PathToRegexpOptions; +} + +declare type RouteRecord = { + path: string; + regex: RouteRegExp; + components: Dictionary; + instances: Dictionary; + name: ?string; + parent: ?RouteRecord; + redirect: ?RedirectOption; + matchAs: ?string; + beforeEnter: ?NavigationGuard; + meta: any; + props: boolean | Object | Function | Dictionary; +} + +declare type Location = { + _normalized?: boolean; + name?: string; + path?: string; + hash?: string; + query?: Dictionary; + params?: Dictionary; + append?: boolean; + replace?: boolean; +} + +declare type RawLocation = string | Location + +declare type Route = { + path: string; + name: ?string; + hash: string; + query: Dictionary; + params: Dictionary; + fullPath: string; + matched: Array; + redirectedFrom?: string; + meta?: any; +} diff --git a/2.Vue-router/src/components/link.js b/2.Vue-router/src/components/link.js new file mode 100644 index 0000000..2662d82 --- /dev/null +++ b/2.Vue-router/src/components/link.js @@ -0,0 +1,137 @@ +/* @flow */ + +import { createRoute, isSameRoute, isIncludedRoute } from '../util/route' +import { extend } from '../util/misc' + +// work around weird flow bug +const toTypes: Array = [String, Object] +const eventTypes: Array = [String, Array] + +export default { + name: 'RouterLink', + props: { + to: { + type: toTypes, + required: true + }, + tag: { + type: String, + default: 'a' + }, + exact: Boolean, + append: Boolean, + replace: Boolean, + activeClass: String, + exactActiveClass: String, + event: { + type: eventTypes, + default: 'click' + } + }, + render (h: Function) { + const router = this.$router + const current = this.$route + const { location, route, href } = router.resolve(this.to, current, this.append) + + const classes = {} + const globalActiveClass = router.options.linkActiveClass + const globalExactActiveClass = router.options.linkExactActiveClass + // Support global empty active class + const activeClassFallback = globalActiveClass == null + ? 'router-link-active' + : globalActiveClass + const exactActiveClassFallback = globalExactActiveClass == null + ? 'router-link-exact-active' + : globalExactActiveClass + const activeClass = this.activeClass == null + ? activeClassFallback + : this.activeClass + const exactActiveClass = this.exactActiveClass == null + ? exactActiveClassFallback + : this.exactActiveClass + const compareTarget = location.path + ? createRoute(null, location, null, router) + : route + + classes[exactActiveClass] = isSameRoute(current, compareTarget) + classes[activeClass] = this.exact + ? classes[exactActiveClass] + : isIncludedRoute(current, compareTarget) + + const handler = e => { + if (guardEvent(e)) { + if (this.replace) { + router.replace(location) + } else { + router.push(location) + } + } + } + + const on = { click: guardEvent } + if (Array.isArray(this.event)) { + this.event.forEach(e => { on[e] = handler }) + } else { + on[this.event] = handler + } + + const data: any = { + class: classes + } + + if (this.tag === 'a') { + data.on = on + data.attrs = { href } + } else { + // find the first child and apply listener and href + const a = findAnchor(this.$slots.default) + if (a) { + // in case the is a static node + a.isStatic = false + const aData = a.data = extend({}, a.data) + aData.on = on + const aAttrs = a.data.attrs = extend({}, a.data.attrs) + aAttrs.href = href + } else { + // doesn't have child, apply listener to self + data.on = on + } + } + + return h(this.tag, data, this.$slots.default) + } +} + +function guardEvent (e) { + // don't redirect with control keys + if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return + // don't redirect when preventDefault called + if (e.defaultPrevented) return + // don't redirect on right click + if (e.button !== undefined && e.button !== 0) return + // don't redirect if `target="_blank"` + if (e.currentTarget && e.currentTarget.getAttribute) { + const target = e.currentTarget.getAttribute('target') + if (/\b_blank\b/i.test(target)) return + } + // this may be a Weex event which doesn't have this method + if (e.preventDefault) { + e.preventDefault() + } + return true +} + +function findAnchor (children) { + if (children) { + let child + for (let i = 0; i < children.length; i++) { + child = children[i] + if (child.tag === 'a') { + return child + } + if (child.children && (child = findAnchor(child.children))) { + return child + } + } + } +} diff --git a/2.Vue-router/src/components/view.js b/2.Vue-router/src/components/view.js new file mode 100644 index 0000000..3f440d5 --- /dev/null +++ b/2.Vue-router/src/components/view.js @@ -0,0 +1,124 @@ +import { warn } from '../util/warn' +import { extend } from '../util/misc' + +export default { + name: 'RouterView', + functional: true, + props: { + name: { + type: String, + default: 'default' + } + }, + render (_, { props, children, parent, data }) { + // used by devtools to display a router-view badge + data.routerView = true + + // directly use parent context's createElement() function + // so that components rendered by router-view can resolve named slots + const h = parent.$createElement + const name = props.name + const route = parent.$route + const cache = parent._routerViewCache || (parent._routerViewCache = {}) + + // determine current view depth, also check to see if the tree + // has been toggled inactive but kept-alive. + let depth = 0 + let inactive = false + while (parent && parent._routerRoot !== parent) { + const vnodeData = parent.$vnode && parent.$vnode.data + if (vnodeData) { + if (vnodeData.routerView) { + depth++ + } + if (vnodeData.keepAlive && parent._inactive) { + inactive = true + } + } + parent = parent.$parent + } + data.routerViewDepth = depth + + // render previous view if the tree is inactive and kept-alive + if (inactive) { + return h(cache[name], data, children) + } + + const matched = route.matched[depth] + // render empty node if no matched route + if (!matched) { + cache[name] = null + return h() + } + + const component = cache[name] = matched.components[name] + + // attach instance registration hook + // this will be called in the instance's injected lifecycle hooks + data.registerRouteInstance = (vm, val) => { + // val could be undefined for unregistration + const current = matched.instances[name] + if ( + (val && current !== vm) || + (!val && current === vm) + ) { + matched.instances[name] = val + } + } + + // also register instance in prepatch hook + // in case the same component instance is reused across different routes + ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => { + matched.instances[name] = vnode.componentInstance + } + + // register instance in init hook + // in case kept-alive component be actived when routes changed + data.hook.init = (vnode) => { + if (vnode.data.keepAlive && + vnode.componentInstance && + vnode.componentInstance !== matched.instances[name] + ) { + matched.instances[name] = vnode.componentInstance + } + } + + // resolve props + let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]) + if (propsToPass) { + // clone to prevent mutation + propsToPass = data.props = extend({}, propsToPass) + // pass non-declared props as attrs + const attrs = data.attrs = data.attrs || {} + for (const key in propsToPass) { + if (!component.props || !(key in component.props)) { + attrs[key] = propsToPass[key] + delete propsToPass[key] + } + } + } + + return h(component, data, children) + } +} + +function resolveProps (route, config) { + switch (typeof config) { + case 'undefined': + return + case 'object': + return config + case 'function': + return config(route) + case 'boolean': + return config ? route.params : undefined + default: + if (process.env.NODE_ENV !== 'production') { + warn( + false, + `props in "${route.path}" is a ${typeof config}, ` + + `expecting an object, function or boolean.` + ) + } + } +} diff --git a/2.Vue-router/src/create-matcher.js b/2.Vue-router/src/create-matcher.js new file mode 100644 index 0000000..07987c4 --- /dev/null +++ b/2.Vue-router/src/create-matcher.js @@ -0,0 +1,202 @@ +/* @flow */ + +import type VueRouter from './index' +import { resolvePath } from './util/path' +import { assert, warn } from './util/warn' +import { createRoute } from './util/route' +import { fillParams } from './util/params' +import { createRouteMap } from './create-route-map' +import { normalizeLocation } from './util/location' + +export type Matcher = { + match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route; + addRoutes: (routes: Array) => void; +}; + +export function createMatcher ( + routes: Array, + router: VueRouter +): Matcher { + const { pathList, pathMap, nameMap } = createRouteMap(routes) + + function addRoutes (routes) { + createRouteMap(routes, pathList, pathMap, nameMap) + } + + function match ( + raw: RawLocation, + currentRoute?: Route, + redirectedFrom?: Location + ): Route { + const location = normalizeLocation(raw, currentRoute, false, router) + const { name } = location + + if (name) { + const record = nameMap[name] + if (process.env.NODE_ENV !== 'production') { + warn(record, `Route with name '${name}' does not exist`) + } + if (!record) return _createRoute(null, location) + const paramNames = record.regex.keys + .filter(key => !key.optional) + .map(key => key.name) + + if (typeof location.params !== 'object') { + location.params = {} + } + + if (currentRoute && typeof currentRoute.params === 'object') { + for (const key in currentRoute.params) { + if (!(key in location.params) && paramNames.indexOf(key) > -1) { + location.params[key] = currentRoute.params[key] + } + } + } + + if (record) { + location.path = fillParams(record.path, location.params, `named route "${name}"`) + return _createRoute(record, location, redirectedFrom) + } + } else if (location.path) { + location.params = {} + for (let i = 0; i < pathList.length; i++) { + const path = pathList[i] + const record = pathMap[path] + if (matchRoute(record.regex, location.path, location.params)) { + return _createRoute(record, location, redirectedFrom) + } + } + } + // no match + return _createRoute(null, location) + } + + function redirect ( + record: RouteRecord, + location: Location + ): Route { + const originalRedirect = record.redirect + let redirect = typeof originalRedirect === 'function' + ? originalRedirect(createRoute(record, location, null, router)) + : originalRedirect + + if (typeof redirect === 'string') { + redirect = { path: redirect } + } + + if (!redirect || typeof redirect !== 'object') { + if (process.env.NODE_ENV !== 'production') { + warn( + false, `invalid redirect option: ${JSON.stringify(redirect)}` + ) + } + return _createRoute(null, location) + } + + const re: Object = redirect + const { name, path } = re + let { query, hash, params } = location + query = re.hasOwnProperty('query') ? re.query : query + hash = re.hasOwnProperty('hash') ? re.hash : hash + params = re.hasOwnProperty('params') ? re.params : params + + if (name) { + // resolved named direct + const targetRecord = nameMap[name] + if (process.env.NODE_ENV !== 'production') { + assert(targetRecord, `redirect failed: named route "${name}" not found.`) + } + return match({ + _normalized: true, + name, + query, + hash, + params + }, undefined, location) + } else if (path) { + // 1. resolve relative redirect + const rawPath = resolveRecordPath(path, record) + // 2. resolve params + const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`) + // 3. rematch with existing query and hash + return match({ + _normalized: true, + path: resolvedPath, + query, + hash + }, undefined, location) + } else { + if (process.env.NODE_ENV !== 'production') { + warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`) + } + return _createRoute(null, location) + } + } + + function alias ( + record: RouteRecord, + location: Location, + matchAs: string + ): Route { + const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`) + const aliasedMatch = match({ + _normalized: true, + path: aliasedPath + }) + if (aliasedMatch) { + const matched = aliasedMatch.matched + const aliasedRecord = matched[matched.length - 1] + location.params = aliasedMatch.params + return _createRoute(aliasedRecord, location) + } + return _createRoute(null, location) + } + + function _createRoute ( + record: ?RouteRecord, + location: Location, + redirectedFrom?: Location + ): Route { + if (record && record.redirect) { + return redirect(record, redirectedFrom || location) + } + if (record && record.matchAs) { + return alias(record, location, record.matchAs) + } + return createRoute(record, location, redirectedFrom, router) + } + + return { + match, + addRoutes + } +} + +function matchRoute ( + regex: RouteRegExp, + path: string, + params: Object +): boolean { + const m = path.match(regex) + + if (!m) { + return false + } else if (!params) { + return true + } + + for (let i = 1, len = m.length; i < len; ++i) { + const key = regex.keys[i - 1] + const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i] + if (key) { + // Fix #1994: using * with props: true generates a param named 0 + params[key.name || 'pathMatch'] = val + } + } + + return true +} + +function resolveRecordPath (path: string, record: RouteRecord): string { + return resolvePath(path, record.parent ? record.parent.path : '/', true) +} diff --git a/2.Vue-router/src/create-route-map.js b/2.Vue-router/src/create-route-map.js new file mode 100644 index 0000000..560e05f --- /dev/null +++ b/2.Vue-router/src/create-route-map.js @@ -0,0 +1,171 @@ +/* @flow */ + +import Regexp from 'path-to-regexp' +import { cleanPath } from './util/path' +import { assert, warn } from './util/warn' + +export function createRouteMap ( + routes: Array, + oldPathList?: Array, + oldPathMap?: Dictionary, + oldNameMap?: Dictionary +): { + pathList: Array; + pathMap: Dictionary; + nameMap: Dictionary; +} { + // the path list is used to control path matching priority + const pathList: Array = oldPathList || [] + // $flow-disable-line + const pathMap: Dictionary = oldPathMap || Object.create(null) + // $flow-disable-line + const nameMap: Dictionary = oldNameMap || Object.create(null) + + routes.forEach(route => { + addRouteRecord(pathList, pathMap, nameMap, route) + }) + + // ensure wildcard routes are always at the end + for (let i = 0, l = pathList.length; i < l; i++) { + if (pathList[i] === '*') { + pathList.push(pathList.splice(i, 1)[0]) + l-- + i-- + } + } + + return { + pathList, + pathMap, + nameMap + } +} + +function addRouteRecord ( + pathList: Array, + pathMap: Dictionary, + nameMap: Dictionary, + route: RouteConfig, + parent?: RouteRecord, + matchAs?: string +) { + const { path, name } = route + if (process.env.NODE_ENV !== 'production') { + assert(path != null, `"path" is required in a route configuration.`) + assert( + typeof route.component !== 'string', + `route config "component" for path: ${String(path || name)} cannot be a ` + + `string id. Use an actual component instead.` + ) + } + + const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {} + const normalizedPath = normalizePath( + path, + parent, + pathToRegexpOptions.strict + ) + + if (typeof route.caseSensitive === 'boolean') { + pathToRegexpOptions.sensitive = route.caseSensitive + } + + const record: RouteRecord = { + path: normalizedPath, + regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), + components: route.components || { default: route.component }, + instances: {}, + name, + parent, + matchAs, + redirect: route.redirect, + beforeEnter: route.beforeEnter, + meta: route.meta || {}, + props: route.props == null + ? {} + : route.components + ? route.props + : { default: route.props } + } + + if (route.children) { + // Warn if route is named, does not redirect and has a default child route. + // If users navigate to this route by name, the default child will + // not be rendered (GH Issue #629) + if (process.env.NODE_ENV !== 'production') { + if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) { + warn( + false, + `Named Route '${route.name}' has a default child route. ` + + `When navigating to this named route (:to="{name: '${route.name}'"), ` + + `the default child route will not be rendered. Remove the name from ` + + `this route and use the name of the default child route for named ` + + `links instead.` + ) + } + } + route.children.forEach(child => { + const childMatchAs = matchAs + ? cleanPath(`${matchAs}/${child.path}`) + : undefined + addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs) + }) + } + + if (route.alias !== undefined) { + const aliases = Array.isArray(route.alias) + ? route.alias + : [route.alias] + + aliases.forEach(alias => { + const aliasRoute = { + path: alias, + children: route.children + } + addRouteRecord( + pathList, + pathMap, + nameMap, + aliasRoute, + parent, + record.path || '/' // matchAs + ) + }) + } + + if (!pathMap[record.path]) { + pathList.push(record.path) + pathMap[record.path] = record + } + + if (name) { + if (!nameMap[name]) { + nameMap[name] = record + } else if (process.env.NODE_ENV !== 'production' && !matchAs) { + warn( + false, + `Duplicate named routes definition: ` + + `{ name: "${name}", path: "${record.path}" }` + ) + } + } +} + +function compileRouteRegex (path: string, pathToRegexpOptions: PathToRegexpOptions): RouteRegExp { + const regex = Regexp(path, [], pathToRegexpOptions) + if (process.env.NODE_ENV !== 'production') { + const keys: any = Object.create(null) + regex.keys.forEach(key => { + warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`) + keys[key.name] = true + }) + } + return regex +} + +function normalizePath (path: string, parent?: RouteRecord, strict?: boolean): string { + if (!strict) path = path.replace(/\/$/, '') + if (path[0] === '/') return path + if (parent == null) return path + return cleanPath(`${parent.path}/${path}`) +} diff --git a/2.Vue-router/src/history/abstract.js b/2.Vue-router/src/history/abstract.js new file mode 100644 index 0000000..00e0939 --- /dev/null +++ b/2.Vue-router/src/history/abstract.js @@ -0,0 +1,51 @@ +/* @flow */ + +import type Router from '../index' +import { History } from './base' + +export class AbstractHistory extends History { + index: number; + stack: Array; + + constructor (router: Router, base: ?string) { + super(router, base) + this.stack = [] + this.index = -1 + } + + push (location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.transitionTo(location, route => { + this.stack = this.stack.slice(0, this.index + 1).concat(route) + this.index++ + onComplete && onComplete(route) + }, onAbort) + } + + replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.transitionTo(location, route => { + this.stack = this.stack.slice(0, this.index).concat(route) + onComplete && onComplete(route) + }, onAbort) + } + + go (n: number) { + const targetIndex = this.index + n + if (targetIndex < 0 || targetIndex >= this.stack.length) { + return + } + const route = this.stack[targetIndex] + this.confirmTransition(route, () => { + this.index = targetIndex + this.updateRoute(route) + }) + } + + getCurrentLocation () { + const current = this.stack[this.stack.length - 1] + return current ? current.fullPath : '/' + } + + ensureURL () { + // noop + } +} diff --git a/2.Vue-router/src/history/base.js b/2.Vue-router/src/history/base.js new file mode 100644 index 0000000..5b6f199 --- /dev/null +++ b/2.Vue-router/src/history/base.js @@ -0,0 +1,331 @@ +/* @flow */ + +import { _Vue } from '../install' +import type Router from '../index' +import { inBrowser } from '../util/dom' +import { runQueue } from '../util/async' +import { warn, isError } from '../util/warn' +import { START, isSameRoute } from '../util/route' +import { + flatten, + flatMapComponents, + resolveAsyncComponents +} from '../util/resolve-components' + +export class History { + router: Router; + base: string; + current: Route; + pending: ?Route; + cb: (r: Route) => void; + ready: boolean; + readyCbs: Array; + readyErrorCbs: Array; + errorCbs: Array; + + // implemented by sub-classes + +go: (n: number) => void; + +push: (loc: RawLocation) => void; + +replace: (loc: RawLocation) => void; + +ensureURL: (push?: boolean) => void; + +getCurrentLocation: () => string; + + constructor (router: Router, base: ?string) { + this.router = router + this.base = normalizeBase(base) + // start with a route object that stands for "nowhere" + this.current = START + this.pending = null + this.ready = false + this.readyCbs = [] + this.readyErrorCbs = [] + this.errorCbs = [] + } + + listen (cb: Function) { + this.cb = cb + } + + onReady (cb: Function, errorCb: ?Function) { + if (this.ready) { + cb() + } else { + this.readyCbs.push(cb) + if (errorCb) { + this.readyErrorCbs.push(errorCb) + } + } + } + + onError (errorCb: Function) { + this.errorCbs.push(errorCb) + } + + transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const route = this.router.match(location, this.current) + this.confirmTransition(route, () => { + this.updateRoute(route) + onComplete && onComplete(route) + this.ensureURL() + + // fire ready cbs once + if (!this.ready) { + this.ready = true + this.readyCbs.forEach(cb => { cb(route) }) + } + }, err => { + if (onAbort) { + onAbort(err) + } + if (err && !this.ready) { + this.ready = true + this.readyErrorCbs.forEach(cb => { cb(err) }) + } + }) + } + + confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { + const current = this.current + const abort = err => { + if (isError(err)) { + if (this.errorCbs.length) { + this.errorCbs.forEach(cb => { cb(err) }) + } else { + warn(false, 'uncaught error during route navigation:') + console.error(err) + } + } + onAbort && onAbort(err) + } + if ( + isSameRoute(route, current) && + // in the case the route map has been dynamically appended to + route.matched.length === current.matched.length + ) { + this.ensureURL() + return abort() + } + + const { + updated, + deactivated, + activated + } = resolveQueue(this.current.matched, route.matched) + + const queue: Array = [].concat( + // in-component leave guards + extractLeaveGuards(deactivated), + // global before hooks + this.router.beforeHooks, + // in-component update hooks + extractUpdateHooks(updated), + // in-config enter guards + activated.map(m => m.beforeEnter), + // async components + resolveAsyncComponents(activated) + ) + + this.pending = route + const iterator = (hook: NavigationGuard, next) => { + if (this.pending !== route) { + return abort() + } + try { + hook(route, current, (to: any) => { + if (to === false || isError(to)) { + // next(false) -> abort navigation, ensure current URL + this.ensureURL(true) + abort(to) + } else if ( + typeof to === 'string' || + (typeof to === 'object' && ( + typeof to.path === 'string' || + typeof to.name === 'string' + )) + ) { + // next('/') or next({ path: '/' }) -> redirect + abort() + if (typeof to === 'object' && to.replace) { + this.replace(to) + } else { + this.push(to) + } + } else { + // confirm transition and pass on the value + next(to) + } + }) + } catch (e) { + abort(e) + } + } + + runQueue(queue, iterator, () => { + const postEnterCbs = [] + const isValid = () => this.current === route + // wait until async components are resolved before + // extracting in-component enter guards + const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) + const queue = enterGuards.concat(this.router.resolveHooks) + runQueue(queue, iterator, () => { + if (this.pending !== route) { + return abort() + } + this.pending = null + onComplete(route) + if (this.router.app) { + this.router.app.$nextTick(() => { + postEnterCbs.forEach(cb => { cb() }) + }) + } + }) + }) + } + + updateRoute (route: Route) { + const prev = this.current + this.current = route + this.cb && this.cb(route) + this.router.afterHooks.forEach(hook => { + hook && hook(route, prev) + }) + } +} + +function normalizeBase (base: ?string): string { + if (!base) { + if (inBrowser) { + // respect tag + const baseEl = document.querySelector('base') + base = (baseEl && baseEl.getAttribute('href')) || '/' + // strip full URL origin + base = base.replace(/^https?:\/\/[^\/]+/, '') + } else { + base = '/' + } + } + // make sure there's the starting slash + if (base.charAt(0) !== '/') { + base = '/' + base + } + // remove trailing slash + return base.replace(/\/$/, '') +} + +function resolveQueue ( + current: Array, + next: Array +): { + updated: Array, + activated: Array, + deactivated: Array +} { + let i + const max = Math.max(current.length, next.length) + for (i = 0; i < max; i++) { + if (current[i] !== next[i]) { + break + } + } + return { + updated: next.slice(0, i), + activated: next.slice(i), + deactivated: current.slice(i) + } +} + +function extractGuards ( + records: Array, + name: string, + bind: Function, + reverse?: boolean +): Array { + const guards = flatMapComponents(records, (def, instance, match, key) => { + const guard = extractGuard(def, name) + if (guard) { + return Array.isArray(guard) + ? guard.map(guard => bind(guard, instance, match, key)) + : bind(guard, instance, match, key) + } + }) + return flatten(reverse ? guards.reverse() : guards) +} + +function extractGuard ( + def: Object | Function, + key: string +): NavigationGuard | Array { + if (typeof def !== 'function') { + // extend now so that global mixins are applied. + def = _Vue.extend(def) + } + return def.options[key] +} + +function extractLeaveGuards (deactivated: Array): Array { + return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true) +} + +function extractUpdateHooks (updated: Array): Array { + return extractGuards(updated, 'beforeRouteUpdate', bindGuard) +} + +function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard { + if (instance) { + return function boundRouteGuard () { + return guard.apply(instance, arguments) + } + } +} + +function extractEnterGuards ( + activated: Array, + cbs: Array, + isValid: () => boolean +): Array { + return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => { + return bindEnterGuard(guard, match, key, cbs, isValid) + }) +} + +function bindEnterGuard ( + guard: NavigationGuard, + match: RouteRecord, + key: string, + cbs: Array, + isValid: () => boolean +): NavigationGuard { + return function routeEnterGuard (to, from, next) { + return guard(to, from, cb => { + next(cb) + if (typeof cb === 'function') { + cbs.push(() => { + // #750 + // if a router-view is wrapped with an out-in transition, + // the instance may not have been registered at this time. + // we will need to poll for registration until current route + // is no longer valid. + poll(cb, match.instances, key, isValid) + }) + } + }) + } +} + +function poll ( + cb: any, // somehow flow cannot infer this is a function + instances: Object, + key: string, + isValid: () => boolean +) { + if ( + instances[key] && + !instances[key]._isBeingDestroyed // do not reuse being destroyed instance + ) { + cb(instances[key]) + } else if (isValid()) { + setTimeout(() => { + poll(cb, instances, key, isValid) + }, 16) + } +} diff --git a/2.Vue-router/src/history/hash.js b/2.Vue-router/src/history/hash.js new file mode 100644 index 0000000..16c94db --- /dev/null +++ b/2.Vue-router/src/history/hash.js @@ -0,0 +1,145 @@ +/* @flow */ + +import type Router from '../index' +import { History } from './base' +import { cleanPath } from '../util/path' +import { getLocation } from './html5' +import { setupScroll, handleScroll } from '../util/scroll' +import { pushState, replaceState, supportsPushState } from '../util/push-state' + +export class HashHistory extends History { + constructor (router: Router, base: ?string, fallback: boolean) { + super(router, base) + // check history fallback deeplinking + if (fallback && checkFallback(this.base)) { + return + } + ensureSlash() + } + + // this is delayed until the app mounts + // to avoid the hashchange listener being fired too early + setupListeners () { + const router = this.router + const expectScroll = router.options.scrollBehavior + const supportsScroll = supportsPushState && expectScroll + + if (supportsScroll) { + setupScroll() + } + + window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { + const current = this.current + if (!ensureSlash()) { + return + } + this.transitionTo(getHash(), route => { + if (supportsScroll) { + handleScroll(this.router, route, current, true) + } + if (!supportsPushState) { + replaceHash(route.fullPath) + } + }) + }) + } + + push (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const { current: fromRoute } = this + this.transitionTo(location, route => { + pushHash(route.fullPath) + handleScroll(this.router, route, fromRoute, false) + onComplete && onComplete(route) + }, onAbort) + } + + replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const { current: fromRoute } = this + this.transitionTo(location, route => { + replaceHash(route.fullPath) + handleScroll(this.router, route, fromRoute, false) + onComplete && onComplete(route) + }, onAbort) + } + + go (n: number) { + window.history.go(n) + } + + ensureURL (push?: boolean) { + const current = this.current.fullPath + if (getHash() !== current) { + push ? pushHash(current) : replaceHash(current) + } + } + + getCurrentLocation () { + return getHash() + } +} + +function checkFallback (base) { + const location = getLocation(base) + if (!/^\/#/.test(location)) { + window.location.replace( + cleanPath(base + '/#' + location) + ) + return true + } +} + +function ensureSlash (): boolean { + const path = getHash() + if (path.charAt(0) === '/') { + return true + } + replaceHash('/' + path) + return false +} + +export function getHash (): string { + // We can't use window.location.hash here because it's not + // consistent across browsers - Firefox will pre-decode it! + let href = window.location.href + const index = href.indexOf('#') + // empty path + if (index < 0) return '' + + href = href.slice(index + 1) + // decode the hash but not the search or hash + // as search(query) is already decoded + // https://github.com/vuejs/vue-router/issues/2708 + const searchIndex = href.indexOf('?') + if (searchIndex < 0) { + const hashIndex = href.indexOf('#') + if (hashIndex > -1) href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex) + else href = decodeURI(href) + } else { + if (searchIndex > -1) href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex) + } + + return href +} + +function getUrl (path) { + const href = window.location.href + const i = href.indexOf('#') + const base = i >= 0 ? href.slice(0, i) : href + return `${base}#${path}` +} + +function pushHash (path) { + if (supportsPushState) { + pushState(getUrl(path)) + } else { + window.location.hash = path + } +} + +function replaceHash (path) { + if (supportsPushState) { + replaceState(getUrl(path)) + } else { + window.location.replace(getUrl(path)) + } +} diff --git a/2.Vue-router/src/history/html5.js b/2.Vue-router/src/history/html5.js new file mode 100644 index 0000000..e1cdba9 --- /dev/null +++ b/2.Vue-router/src/history/html5.js @@ -0,0 +1,80 @@ +/* @flow */ + +import type Router from '../index' +import { History } from './base' +import { cleanPath } from '../util/path' +import { START } from '../util/route' +import { setupScroll, handleScroll } from '../util/scroll' +import { pushState, replaceState, supportsPushState } from '../util/push-state' + +export class HTML5History extends History { + constructor (router: Router, base: ?string) { + super(router, base) + + const expectScroll = router.options.scrollBehavior + const supportsScroll = supportsPushState && expectScroll + + if (supportsScroll) { + setupScroll() + } + + const initLocation = getLocation(this.base) + window.addEventListener('popstate', e => { + const current = this.current + + // Avoiding first `popstate` event dispatched in some browsers but first + // history route not updated since async guard at the same time. + const location = getLocation(this.base) + if (this.current === START && location === initLocation) { + return + } + + this.transitionTo(location, route => { + if (supportsScroll) { + handleScroll(router, route, current, true) + } + }) + }) + } + + go (n: number) { + window.history.go(n) + } + + push (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const { current: fromRoute } = this + this.transitionTo(location, route => { + pushState(cleanPath(this.base + route.fullPath)) + handleScroll(this.router, route, fromRoute, false) + onComplete && onComplete(route) + }, onAbort) + } + + replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const { current: fromRoute } = this + this.transitionTo(location, route => { + replaceState(cleanPath(this.base + route.fullPath)) + handleScroll(this.router, route, fromRoute, false) + onComplete && onComplete(route) + }, onAbort) + } + + ensureURL (push?: boolean) { + if (getLocation(this.base) !== this.current.fullPath) { + const current = cleanPath(this.base + this.current.fullPath) + push ? pushState(current) : replaceState(current) + } + } + + getCurrentLocation (): string { + return getLocation(this.base) + } +} + +export function getLocation (base: string): string { + let path = decodeURI(window.location.pathname) + if (base && path.indexOf(base) === 0) { + path = path.slice(base.length) + } + return (path || '/') + window.location.search + window.location.hash +} diff --git a/2.Vue-router/src/index.js b/2.Vue-router/src/index.js new file mode 100644 index 0000000..b3eb25d --- /dev/null +++ b/2.Vue-router/src/index.js @@ -0,0 +1,248 @@ +/* @flow */ + +import { install } from './install' +import { START } from './util/route' +import { assert } from './util/warn' +import { inBrowser } from './util/dom' +import { cleanPath } from './util/path' +import { createMatcher } from './create-matcher' +import { normalizeLocation } from './util/location' +import { supportsPushState } from './util/push-state' + +import { HashHistory } from './history/hash' +import { HTML5History } from './history/html5' +import { AbstractHistory } from './history/abstract' + +import type { Matcher } from './create-matcher' + +export default class VueRouter { + static install: () => void; + static version: string; + + app: any; + apps: Array; + ready: boolean; + readyCbs: Array; + options: RouterOptions; + mode: string; + history: HashHistory | HTML5History | AbstractHistory; + matcher: Matcher; + fallback: boolean; + beforeHooks: Array; + resolveHooks: Array; + afterHooks: Array; + + constructor (options: RouterOptions = {}) { + this.app = null + this.apps = [] + this.options = options + this.beforeHooks = [] + this.resolveHooks = [] + this.afterHooks = [] + this.matcher = createMatcher(options.routes || [], this) + + let mode = options.mode || 'hash' + this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false + if (this.fallback) { + mode = 'hash' + } + if (!inBrowser) { + mode = 'abstract' + } + this.mode = mode + + switch (mode) { + case 'history': + this.history = new HTML5History(this, options.base) + break + case 'hash': + this.history = new HashHistory(this, options.base, this.fallback) + break + case 'abstract': + this.history = new AbstractHistory(this, options.base) + break + default: + if (process.env.NODE_ENV !== 'production') { + assert(false, `invalid mode: ${mode}`) + } + } + } + + match ( + raw: RawLocation, + current?: Route, + redirectedFrom?: Location + ): Route { + return this.matcher.match(raw, current, redirectedFrom) + } + + get currentRoute (): ?Route { + return this.history && this.history.current + } + + init (app: any /* Vue component instance */) { + process.env.NODE_ENV !== 'production' && assert( + install.installed, + `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + + `before creating root instance.` + ) + + this.apps.push(app) + + // set up app destroyed handler + // https://github.com/vuejs/vue-router/issues/2639 + app.$once('hook:destroyed', () => { + // clean out app from this.apps array once destroyed + const index = this.apps.indexOf(app) + if (index > -1) this.apps.splice(index, 1) + // ensure we still have a main app or null if no apps + // we do not release the router so it can be reused + if (this.app === app) this.app = this.apps[0] || null + }) + + // main app previously initialized + // return as we don't need to set up new history listener + if (this.app) { + return + } + + this.app = app + + const history = this.history + + if (history instanceof HTML5History) { + history.transitionTo(history.getCurrentLocation()) + } else if (history instanceof HashHistory) { + const setupHashListener = () => { + history.setupListeners() + } + history.transitionTo( + history.getCurrentLocation(), + setupHashListener, + setupHashListener + ) + } + + history.listen(route => { + this.apps.forEach((app) => { + app._route = route + }) + }) + } + + beforeEach (fn: Function): Function { + return registerHook(this.beforeHooks, fn) + } + + beforeResolve (fn: Function): Function { + return registerHook(this.resolveHooks, fn) + } + + afterEach (fn: Function): Function { + return registerHook(this.afterHooks, fn) + } + + onReady (cb: Function, errorCb?: Function) { + this.history.onReady(cb, errorCb) + } + + onError (errorCb: Function) { + this.history.onError(errorCb) + } + + push (location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.history.push(location, onComplete, onAbort) + } + + replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.history.replace(location, onComplete, onAbort) + } + + go (n: number) { + this.history.go(n) + } + + back () { + this.go(-1) + } + + forward () { + this.go(1) + } + + getMatchedComponents (to?: RawLocation | Route): Array { + const route: any = to + ? to.matched + ? to + : this.resolve(to).route + : this.currentRoute + if (!route) { + return [] + } + return [].concat.apply([], route.matched.map(m => { + return Object.keys(m.components).map(key => { + return m.components[key] + }) + })) + } + + resolve ( + to: RawLocation, + current?: Route, + append?: boolean + ): { + location: Location, + route: Route, + href: string, + // for backwards compat + normalizedTo: Location, + resolved: Route + } { + current = current || this.history.current + const location = normalizeLocation( + to, + current, + append, + this + ) + const route = this.match(location, current) + const fullPath = route.redirectedFrom || route.fullPath + const base = this.history.base + const href = createHref(base, fullPath, this.mode) + return { + location, + route, + href, + // for backwards compat + normalizedTo: location, + resolved: route + } + } + + addRoutes (routes: Array) { + this.matcher.addRoutes(routes) + if (this.history.current !== START) { + this.history.transitionTo(this.history.getCurrentLocation()) + } + } +} + +function registerHook (list: Array, fn: Function): Function { + list.push(fn) + return () => { + const i = list.indexOf(fn) + if (i > -1) list.splice(i, 1) + } +} + +function createHref (base: string, fullPath: string, mode) { + var path = mode === 'hash' ? '#' + fullPath : fullPath + return base ? cleanPath(base + '/' + path) : path +} + +VueRouter.install = install +VueRouter.version = '__VERSION__' + +if (inBrowser && window.Vue) { + window.Vue.use(VueRouter) +} diff --git a/2.Vue-router/src/install.js b/2.Vue-router/src/install.js new file mode 100644 index 0000000..ee8506a --- /dev/null +++ b/2.Vue-router/src/install.js @@ -0,0 +1,52 @@ +import View from './components/view' +import Link from './components/link' + +export let _Vue + +export function install (Vue) { + if (install.installed && _Vue === Vue) return + install.installed = true + + _Vue = Vue + + const isDef = v => v !== undefined + + const registerInstance = (vm, callVal) => { + let i = vm.$options._parentVnode + if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { + i(vm, callVal) + } + } + + Vue.mixin({ + beforeCreate () { + if (isDef(this.$options.router)) { + this._routerRoot = this + this._router = this.$options.router + this._router.init(this) + Vue.util.defineReactive(this, '_route', this._router.history.current) + } else { + this._routerRoot = (this.$parent && this.$parent._routerRoot) || this + } + registerInstance(this, this) + }, + destroyed () { + registerInstance(this) + } + }) + + Object.defineProperty(Vue.prototype, '$router', { + get () { return this._routerRoot._router } + }) + + Object.defineProperty(Vue.prototype, '$route', { + get () { return this._routerRoot._route } + }) + + Vue.component('RouterView', View) + Vue.component('RouterLink', Link) + + const strats = Vue.config.optionMergeStrategies + // use the same hook merging strategy for route hooks + strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created +} diff --git a/2.Vue-router/src/util/async.js b/2.Vue-router/src/util/async.js new file mode 100644 index 0000000..03bbf60 --- /dev/null +++ b/2.Vue-router/src/util/async.js @@ -0,0 +1,18 @@ +/* @flow */ + +export function runQueue (queue: Array, fn: Function, cb: Function) { + const step = index => { + if (index >= queue.length) { + cb() + } else { + if (queue[index]) { + fn(queue[index], () => { + step(index + 1) + }) + } else { + step(index + 1) + } + } + } + step(0) +} diff --git a/2.Vue-router/src/util/dom.js b/2.Vue-router/src/util/dom.js new file mode 100644 index 0000000..d497b15 --- /dev/null +++ b/2.Vue-router/src/util/dom.js @@ -0,0 +1,3 @@ +/* @flow */ + +export const inBrowser = typeof window !== 'undefined' diff --git a/2.Vue-router/src/util/location.js b/2.Vue-router/src/util/location.js new file mode 100644 index 0000000..176b501 --- /dev/null +++ b/2.Vue-router/src/util/location.js @@ -0,0 +1,64 @@ +/* @flow */ + +import type VueRouter from '../index' +import { parsePath, resolvePath } from './path' +import { resolveQuery } from './query' +import { fillParams } from './params' +import { warn } from './warn' +import { extend } from './misc' + +export function normalizeLocation ( + raw: RawLocation, + current: ?Route, + append: ?boolean, + router: ?VueRouter +): Location { + let next: Location = typeof raw === 'string' ? { path: raw } : raw + // named target + if (next._normalized) { + return next + } else if (next.name) { + return extend({}, raw) + } + + // relative params + if (!next.path && next.params && current) { + next = extend({}, next) + next._normalized = true + const params: any = extend(extend({}, current.params), next.params) + if (current.name) { + next.name = current.name + next.params = params + } else if (current.matched.length) { + const rawPath = current.matched[current.matched.length - 1].path + next.path = fillParams(rawPath, params, `path ${current.path}`) + } else if (process.env.NODE_ENV !== 'production') { + warn(false, `relative params navigation requires a current route.`) + } + return next + } + + const parsedPath = parsePath(next.path || '') + const basePath = (current && current.path) || '/' + const path = parsedPath.path + ? resolvePath(parsedPath.path, basePath, append || next.append) + : basePath + + const query = resolveQuery( + parsedPath.query, + next.query, + router && router.options.parseQuery + ) + + let hash = next.hash || parsedPath.hash + if (hash && hash.charAt(0) !== '#') { + hash = `#${hash}` + } + + return { + _normalized: true, + path, + query, + hash + } +} diff --git a/2.Vue-router/src/util/misc.js b/2.Vue-router/src/util/misc.js new file mode 100644 index 0000000..fb22650 --- /dev/null +++ b/2.Vue-router/src/util/misc.js @@ -0,0 +1,6 @@ +export function extend (a, b) { + for (const key in b) { + a[key] = b[key] + } + return a +} diff --git a/2.Vue-router/src/util/params.js b/2.Vue-router/src/util/params.js new file mode 100644 index 0000000..cac6a68 --- /dev/null +++ b/2.Vue-router/src/util/params.js @@ -0,0 +1,35 @@ +/* @flow */ + +import { warn } from './warn' +import Regexp from 'path-to-regexp' + +// $flow-disable-line +const regexpCompileCache: { + [key: string]: Function +} = Object.create(null) + +export function fillParams ( + path: string, + params: ?Object, + routeMsg: string +): string { + params = params || {} + try { + const filler = + regexpCompileCache[path] || + (regexpCompileCache[path] = Regexp.compile(path)) + + // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} + if (params.pathMatch) params[0] = params.pathMatch + + return filler(params, { pretty: true }) + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + warn(false, `missing param for ${routeMsg}: ${e.message}`) + } + return '' + } finally { + // delete the 0 if it was added + delete params[0] + } +} diff --git a/2.Vue-router/src/util/path.js b/2.Vue-router/src/util/path.js new file mode 100644 index 0000000..8e11034 --- /dev/null +++ b/2.Vue-router/src/util/path.js @@ -0,0 +1,74 @@ +/* @flow */ + +export function resolvePath ( + relative: string, + base: string, + append?: boolean +): string { + const firstChar = relative.charAt(0) + if (firstChar === '/') { + return relative + } + + if (firstChar === '?' || firstChar === '#') { + return base + relative + } + + const stack = base.split('/') + + // remove trailing segment if: + // - not appending + // - appending to trailing slash (last segment is empty) + if (!append || !stack[stack.length - 1]) { + stack.pop() + } + + // resolve relative path + const segments = relative.replace(/^\//, '').split('/') + for (let i = 0; i < segments.length; i++) { + const segment = segments[i] + if (segment === '..') { + stack.pop() + } else if (segment !== '.') { + stack.push(segment) + } + } + + // ensure leading slash + if (stack[0] !== '') { + stack.unshift('') + } + + return stack.join('/') +} + +export function parsePath (path: string): { + path: string; + query: string; + hash: string; +} { + let hash = '' + let query = '' + + const hashIndex = path.indexOf('#') + if (hashIndex >= 0) { + hash = path.slice(hashIndex) + path = path.slice(0, hashIndex) + } + + const queryIndex = path.indexOf('?') + if (queryIndex >= 0) { + query = path.slice(queryIndex + 1) + path = path.slice(0, queryIndex) + } + + return { + path, + query, + hash + } +} + +export function cleanPath (path: string): string { + return path.replace(/\/\//g, '/') +} diff --git a/2.Vue-router/src/util/push-state.js b/2.Vue-router/src/util/push-state.js new file mode 100644 index 0000000..136d6f9 --- /dev/null +++ b/2.Vue-router/src/util/push-state.js @@ -0,0 +1,59 @@ +/* @flow */ + +import { inBrowser } from './dom' +import { saveScrollPosition } from './scroll' + +export const supportsPushState = inBrowser && (function () { + const ua = window.navigator.userAgent + + if ( + (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && + ua.indexOf('Mobile Safari') !== -1 && + ua.indexOf('Chrome') === -1 && + ua.indexOf('Windows Phone') === -1 + ) { + return false + } + + return window.history && 'pushState' in window.history +})() + +// use User Timing api (if present) for more accurate key precision +const Time = inBrowser && window.performance && window.performance.now + ? window.performance + : Date + +let _key: string = genKey() + +function genKey (): string { + return Time.now().toFixed(3) +} + +export function getStateKey () { + return _key +} + +export function setStateKey (key: string) { + _key = key +} + +export function pushState (url?: string, replace?: boolean) { + saveScrollPosition() + // try...catch the pushState call to get around Safari + // DOM Exception 18 where it limits to 100 pushState calls + const history = window.history + try { + if (replace) { + history.replaceState({ key: _key }, '', url) + } else { + _key = genKey() + history.pushState({ key: _key }, '', url) + } + } catch (e) { + window.location[replace ? 'replace' : 'assign'](url) + } +} + +export function replaceState (url?: string) { + pushState(url, true) +} diff --git a/2.Vue-router/src/util/query.js b/2.Vue-router/src/util/query.js new file mode 100644 index 0000000..b9abc21 --- /dev/null +++ b/2.Vue-router/src/util/query.js @@ -0,0 +1,95 @@ +/* @flow */ + +import { warn } from './warn' + +const encodeReserveRE = /[!'()*]/g +const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16) +const commaRE = /%2C/g + +// fixed encodeURIComponent which is more conformant to RFC3986: +// - escapes [!'()*] +// - preserve commas +const encode = str => encodeURIComponent(str) + .replace(encodeReserveRE, encodeReserveReplacer) + .replace(commaRE, ',') + +const decode = decodeURIComponent + +export function resolveQuery ( + query: ?string, + extraQuery: Dictionary = {}, + _parseQuery: ?Function +): Dictionary { + const parse = _parseQuery || parseQuery + let parsedQuery + try { + parsedQuery = parse(query || '') + } catch (e) { + process.env.NODE_ENV !== 'production' && warn(false, e.message) + parsedQuery = {} + } + for (const key in extraQuery) { + parsedQuery[key] = extraQuery[key] + } + return parsedQuery +} + +function parseQuery (query: string): Dictionary { + const res = {} + + query = query.trim().replace(/^(\?|#|&)/, '') + + if (!query) { + return res + } + + query.split('&').forEach(param => { + const parts = param.replace(/\+/g, ' ').split('=') + const key = decode(parts.shift()) + const val = parts.length > 0 + ? decode(parts.join('=')) + : null + + if (res[key] === undefined) { + res[key] = val + } else if (Array.isArray(res[key])) { + res[key].push(val) + } else { + res[key] = [res[key], val] + } + }) + + return res +} + +export function stringifyQuery (obj: Dictionary): string { + const res = obj ? Object.keys(obj).map(key => { + const val = obj[key] + + if (val === undefined) { + return '' + } + + if (val === null) { + return encode(key) + } + + if (Array.isArray(val)) { + const result = [] + val.forEach(val2 => { + if (val2 === undefined) { + return + } + if (val2 === null) { + result.push(encode(key)) + } else { + result.push(encode(key) + '=' + encode(val2)) + } + }) + return result.join('&') + } + + return encode(key) + '=' + encode(val) + }).filter(x => x.length > 0).join('&') : null + return res ? `?${res}` : '' +} diff --git a/2.Vue-router/src/util/resolve-components.js b/2.Vue-router/src/util/resolve-components.js new file mode 100644 index 0000000..3f7608c --- /dev/null +++ b/2.Vue-router/src/util/resolve-components.js @@ -0,0 +1,108 @@ +/* @flow */ + +import { _Vue } from '../install' +import { warn, isError } from './warn' + +export function resolveAsyncComponents (matched: Array): Function { + return (to, from, next) => { + let hasAsync = false + let pending = 0 + let error = null + + flatMapComponents(matched, (def, _, match, key) => { + // if it's a function and doesn't have cid attached, + // assume it's an async component resolve function. + // we are not using Vue's default async resolving mechanism because + // we want to halt the navigation until the incoming component has been + // resolved. + if (typeof def === 'function' && def.cid === undefined) { + hasAsync = true + pending++ + + const resolve = once(resolvedDef => { + if (isESModule(resolvedDef)) { + resolvedDef = resolvedDef.default + } + // save resolved on async factory in case it's used elsewhere + def.resolved = typeof resolvedDef === 'function' + ? resolvedDef + : _Vue.extend(resolvedDef) + match.components[key] = resolvedDef + pending-- + if (pending <= 0) { + next() + } + }) + + const reject = once(reason => { + const msg = `Failed to resolve async component ${key}: ${reason}` + process.env.NODE_ENV !== 'production' && warn(false, msg) + if (!error) { + error = isError(reason) + ? reason + : new Error(msg) + next(error) + } + }) + + let res + try { + res = def(resolve, reject) + } catch (e) { + reject(e) + } + if (res) { + if (typeof res.then === 'function') { + res.then(resolve, reject) + } else { + // new syntax in Vue 2.3 + const comp = res.component + if (comp && typeof comp.then === 'function') { + comp.then(resolve, reject) + } + } + } + } + }) + + if (!hasAsync) next() + } +} + +export function flatMapComponents ( + matched: Array, + fn: Function +): Array { + return flatten(matched.map(m => { + return Object.keys(m.components).map(key => fn( + m.components[key], + m.instances[key], + m, key + )) + })) +} + +export function flatten (arr: Array): Array { + return Array.prototype.concat.apply([], arr) +} + +const hasSymbol = + typeof Symbol === 'function' && + typeof Symbol.toStringTag === 'symbol' + +function isESModule (obj) { + return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module') +} + +// in Webpack 2, require.ensure now also returns a Promise +// so the resolve/reject functions may get called an extra time +// if the user uses an arrow function shorthand that happens to +// return that Promise. +function once (fn) { + let called = false + return function (...args) { + if (called) return + called = true + return fn.apply(this, args) + } +} diff --git a/2.Vue-router/src/util/route.js b/2.Vue-router/src/util/route.js new file mode 100644 index 0000000..54a91a7 --- /dev/null +++ b/2.Vue-router/src/util/route.js @@ -0,0 +1,132 @@ +/* @flow */ + +import type VueRouter from '../index' +import { stringifyQuery } from './query' + +const trailingSlashRE = /\/?$/ + +export function createRoute ( + record: ?RouteRecord, + location: Location, + redirectedFrom?: ?Location, + router?: VueRouter +): Route { + const stringifyQuery = router && router.options.stringifyQuery + + let query: any = location.query || {} + try { + query = clone(query) + } catch (e) {} + + const route: Route = { + name: location.name || (record && record.name), + meta: (record && record.meta) || {}, + path: location.path || '/', + hash: location.hash || '', + query, + params: location.params || {}, + fullPath: getFullPath(location, stringifyQuery), + matched: record ? formatMatch(record) : [] + } + if (redirectedFrom) { + route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery) + } + return Object.freeze(route) +} + +function clone (value) { + if (Array.isArray(value)) { + return value.map(clone) + } else if (value && typeof value === 'object') { + const res = {} + for (const key in value) { + res[key] = clone(value[key]) + } + return res + } else { + return value + } +} + +// the starting route that represents the initial state +export const START = createRoute(null, { + path: '/' +}) + +function formatMatch (record: ?RouteRecord): Array { + const res = [] + while (record) { + res.unshift(record) + record = record.parent + } + return res +} + +function getFullPath ( + { path, query = {}, hash = '' }, + _stringifyQuery +): string { + const stringify = _stringifyQuery || stringifyQuery + return (path || '/') + stringify(query) + hash +} + +export function isSameRoute (a: Route, b: ?Route): boolean { + if (b === START) { + return a === b + } else if (!b) { + return false + } else if (a.path && b.path) { + return ( + a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && + a.hash === b.hash && + isObjectEqual(a.query, b.query) + ) + } else if (a.name && b.name) { + return ( + a.name === b.name && + a.hash === b.hash && + isObjectEqual(a.query, b.query) && + isObjectEqual(a.params, b.params) + ) + } else { + return false + } +} + +function isObjectEqual (a = {}, b = {}): boolean { + // handle null value #1566 + if (!a || !b) return a === b + const aKeys = Object.keys(a) + const bKeys = Object.keys(b) + if (aKeys.length !== bKeys.length) { + return false + } + return aKeys.every(key => { + const aVal = a[key] + const bVal = b[key] + // check nested equality + if (typeof aVal === 'object' && typeof bVal === 'object') { + return isObjectEqual(aVal, bVal) + } + return String(aVal) === String(bVal) + }) +} + +export function isIncludedRoute (current: Route, target: Route): boolean { + return ( + current.path.replace(trailingSlashRE, '/').indexOf( + target.path.replace(trailingSlashRE, '/') + ) === 0 && + (!target.hash || current.hash === target.hash) && + queryIncludes(current.query, target.query) + ) +} + +function queryIncludes (current: Dictionary, target: Dictionary): boolean { + for (const key in target) { + if (!(key in current)) { + return false + } + } + return true +} diff --git a/2.Vue-router/src/util/scroll.js b/2.Vue-router/src/util/scroll.js new file mode 100644 index 0000000..1f4e68f --- /dev/null +++ b/2.Vue-router/src/util/scroll.js @@ -0,0 +1,130 @@ +/* @flow */ + +import type Router from '../index' +import { assert } from './warn' +import { getStateKey, setStateKey } from './push-state' + +const positionStore = Object.create(null) + +export function setupScroll () { + // Fix for #1585 for Firefox + // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678 + window.history.replaceState({ key: getStateKey() }, '', window.location.href.replace(window.location.origin, '')) + window.addEventListener('popstate', e => { + saveScrollPosition() + if (e.state && e.state.key) { + setStateKey(e.state.key) + } + }) +} + +export function handleScroll ( + router: Router, + to: Route, + from: Route, + isPop: boolean +) { + if (!router.app) { + return + } + + const behavior = router.options.scrollBehavior + if (!behavior) { + return + } + + if (process.env.NODE_ENV !== 'production') { + assert(typeof behavior === 'function', `scrollBehavior must be a function`) + } + + // wait until re-render finishes before scrolling + router.app.$nextTick(() => { + const position = getScrollPosition() + const shouldScroll = behavior.call(router, to, from, isPop ? position : null) + + if (!shouldScroll) { + return + } + + if (typeof shouldScroll.then === 'function') { + shouldScroll.then(shouldScroll => { + scrollToPosition((shouldScroll: any), position) + }).catch(err => { + if (process.env.NODE_ENV !== 'production') { + assert(false, err.toString()) + } + }) + } else { + scrollToPosition(shouldScroll, position) + } + }) +} + +export function saveScrollPosition () { + const key = getStateKey() + if (key) { + positionStore[key] = { + x: window.pageXOffset, + y: window.pageYOffset + } + } +} + +function getScrollPosition (): ?Object { + const key = getStateKey() + if (key) { + return positionStore[key] + } +} + +function getElementPosition (el: Element, offset: Object): Object { + const docEl: any = document.documentElement + const docRect = docEl.getBoundingClientRect() + const elRect = el.getBoundingClientRect() + return { + x: elRect.left - docRect.left - offset.x, + y: elRect.top - docRect.top - offset.y + } +} + +function isValidPosition (obj: Object): boolean { + return isNumber(obj.x) || isNumber(obj.y) +} + +function normalizePosition (obj: Object): Object { + return { + x: isNumber(obj.x) ? obj.x : window.pageXOffset, + y: isNumber(obj.y) ? obj.y : window.pageYOffset + } +} + +function normalizeOffset (obj: Object): Object { + return { + x: isNumber(obj.x) ? obj.x : 0, + y: isNumber(obj.y) ? obj.y : 0 + } +} + +function isNumber (v: any): boolean { + return typeof v === 'number' +} + +function scrollToPosition (shouldScroll, position) { + const isObject = typeof shouldScroll === 'object' + if (isObject && typeof shouldScroll.selector === 'string') { + const el = document.querySelector(shouldScroll.selector) + if (el) { + let offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {} + offset = normalizeOffset(offset) + position = getElementPosition(el, offset) + } else if (isValidPosition(shouldScroll)) { + position = normalizePosition(shouldScroll) + } + } else if (isObject && isValidPosition(shouldScroll)) { + position = normalizePosition(shouldScroll) + } + + if (position) { + window.scrollTo(position.x, position.y) + } +} diff --git a/2.Vue-router/src/util/warn.js b/2.Vue-router/src/util/warn.js new file mode 100644 index 0000000..65f1cec --- /dev/null +++ b/2.Vue-router/src/util/warn.js @@ -0,0 +1,17 @@ +/* @flow */ + +export function assert (condition: any, message: string) { + if (!condition) { + throw new Error(`[vue-router] ${message}`) + } +} + +export function warn (condition: any, message: string) { + if (process.env.NODE_ENV !== 'production' && !condition) { + typeof console !== 'undefined' && console.warn(`[vue-router] ${message}`) + } +} + +export function isError (err: any): boolean { + return Object.prototype.toString.call(err).indexOf('Error') > -1 +} diff --git a/2.Vue-router/test/.eslintrc b/2.Vue-router/test/.eslintrc new file mode 100644 index 0000000..52939e2 --- /dev/null +++ b/2.Vue-router/test/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "jasmine": true + } +} diff --git a/2.Vue-router/test/e2e/.eslintrc b/2.Vue-router/test/e2e/.eslintrc new file mode 100644 index 0000000..e5a34ae --- /dev/null +++ b/2.Vue-router/test/e2e/.eslintrc @@ -0,0 +1,5 @@ +{ + "env": { + "browser": true + } +} diff --git a/2.Vue-router/test/e2e/nightwatch.config.js b/2.Vue-router/test/e2e/nightwatch.config.js new file mode 100644 index 0000000..cc8d613 --- /dev/null +++ b/2.Vue-router/test/e2e/nightwatch.config.js @@ -0,0 +1,53 @@ +// http://nightwatchjs.org/guide#settings-file + +module.exports = { + 'src_folders': ['test/e2e/specs'], + 'output_folder': 'test/e2e/reports', + 'custom_commands_path': ['node_modules/nightwatch-helpers/commands'], + 'custom_assertions_path': ['node_modules/nightwatch-helpers/assertions'], + + 'selenium': { + 'start_process': true, + 'server_path': require('selenium-server').path, + 'host': '127.0.0.1', + 'port': 4444, + 'cli_args': { + 'webdriver.chrome.driver': require('chromedriver').path + } + }, + + 'test_settings': { + 'default': { + 'selenium_port': 4444, + 'selenium_host': 'localhost', + 'silent': true, + 'screenshots': { + 'enabled': true, + 'on_failure': true, + 'on_error': false, + 'path': 'test/e2e/screenshots' + } + }, + + 'chrome': { + 'desiredCapabilities': { + 'browserName': 'chrome', + 'javascriptEnabled': true, + 'acceptSslCerts': true, + 'chromeOptions': { + 'args': [ + 'window-size=1280,800' + ] + } + } + }, + + 'phantomjs': { + 'desiredCapabilities': { + 'browserName': 'phantomjs', + 'javascriptEnabled': true, + 'acceptSslCerts': true + } + } + } +} diff --git a/2.Vue-router/test/e2e/runner.js b/2.Vue-router/test/e2e/runner.js new file mode 100644 index 0000000..714a042 --- /dev/null +++ b/2.Vue-router/test/e2e/runner.js @@ -0,0 +1,34 @@ +var spawn = require('cross-spawn') +var args = process.argv.slice(2) + +var server = args.indexOf('--dev') > -1 + ? null + : require('../../examples/server') + +if (args.indexOf('--config') === -1) { + args = args.concat(['--config', 'test/e2e/nightwatch.config.js']) +} +if (args.indexOf('--env') === -1) { + args = args.concat(['--env', 'phantomjs']) +} +var i = args.indexOf('--test') +if (i > -1) { + args[i + 1] = 'test/e2e/specs/' + args[i + 1].replace(/\.js$/, '') + '.js' +} +if (args.indexOf('phantomjs') > -1) { + process.env.PHANTOMJS = true +} + +var runner = spawn('./node_modules/.bin/nightwatch', args, { + stdio: 'inherit' +}) + +runner.on('exit', function (code) { + server && server.close() + process.exit(code) +}) + +runner.on('error', function (err) { + server && server.close() + throw err +}) diff --git a/2.Vue-router/test/e2e/specs/active-links.js b/2.Vue-router/test/e2e/specs/active-links.js new file mode 100644 index 0000000..e40a1c6 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/active-links.js @@ -0,0 +1,54 @@ + +module.exports = { + 'active links': function (browser) { + browser + .url('http://localhost:8080/active-links/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 11) + // assert correct href with base + .assert.attributeContains('li:nth-child(1) a', 'href', '/active-links/') + .assert.attributeContains('li:nth-child(2) a', 'href', '/active-links/') + .assert.attributeContains('li:nth-child(3) a', 'href', '/active-links/users') + .assert.attributeContains('li:nth-child(4) a', 'href', '/active-links/users') + .assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users/evan') + .assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan#foo') + .assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan?foo=bar') + .assert.attributeContains('li:nth-child(8) a', 'href', '/active-links/users/evan?foo=bar') + .assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar&baz=qux') + .assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/about') + .assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/about') + .assert.containsText('.view', 'Home') + + assertActiveLinks(1, [1, 2], null, [1, 2]) + assertActiveLinks(2, [1, 2], null, [1, 2]) + assertActiveLinks(3, [1, 3, 4], null, [3, 4]) + assertActiveLinks(4, [1, 3, 4], null, [3, 4]) + assertActiveLinks(5, [1, 3, 5], null, [5]) + assertActiveLinks(6, [1, 3, 5, 6], null, [6]) + assertActiveLinks(7, [1, 3, 5, 7, 8], null, [7, 8]) + assertActiveLinks(8, [1, 3, 5, 7, 8], null, [7, 8]) + assertActiveLinks(9, [1, 3, 5, 7, 9], null, [9]) + assertActiveLinks(10, [1, 10], [11], [10], [11]) + assertActiveLinks(11, [1, 10], [11], [10], [11]) + + browser.end() + + function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI) { + browser.click(`li:nth-child(${n}) a`) + activeA.forEach(i => { + browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active') + }) + activeLI && activeLI.forEach(i => { + browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active') + }) + exactActiveA.forEach(i => { + browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-exact-active') + .assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active') + }) + exactActiveLI && exactActiveLI.forEach(i => { + browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-active') + .assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active') + }) + } + } +} diff --git a/2.Vue-router/test/e2e/specs/auth-flow.js b/2.Vue-router/test/e2e/specs/auth-flow.js new file mode 100644 index 0000000..7f0f795 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/auth-flow.js @@ -0,0 +1,54 @@ +module.exports = { + 'auth flow': function (browser) { + browser + .url('http://localhost:8080/auth-flow/') + .waitForElementVisible('#app', 1000) + .assert.containsText('#app p', 'You are logged out') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/auth-flow/login?redirect=%2Fdashboard') + .assert.containsText('#app h2', 'Login') + .assert.containsText('#app p', 'You need to login first.') + + .click('button') + .assert.urlEquals('http://localhost:8080/auth-flow/login?redirect=%2Fdashboard') + .assert.elementPresent('.error') + + .setValue('input[type=password]', 'password1') + .click('button') + .assert.urlEquals('http://localhost:8080/auth-flow/dashboard') + .assert.containsText('#app h2', 'Dashboard') + .assert.containsText('#app p', 'Yay you made it!') + + // reload + .url('http://localhost:8080/auth-flow/') + .waitForElementVisible('#app', 1000) + .assert.containsText('#app p', 'You are logged in') + + // navigate when logged in + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/auth-flow/dashboard') + .assert.containsText('#app h2', 'Dashboard') + .assert.containsText('#app p', 'Yay you made it!') + + // directly visit dashboard when logged in + .url('http://localhost:8080/auth-flow/dashboard') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/auth-flow/dashboard') + .assert.containsText('#app h2', 'Dashboard') + .assert.containsText('#app p', 'Yay you made it!') + + // log out + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/auth-flow/') + .assert.containsText('#app p', 'You are logged out') + + // directly visit dashboard when logged out + .url('http://localhost:8080/auth-flow/dashboard') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/auth-flow/login?redirect=%2Fdashboard') + .assert.containsText('#app h2', 'Login') + .assert.containsText('#app p', 'You need to login first.') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/basic.js b/2.Vue-router/test/e2e/specs/basic.js new file mode 100644 index 0000000..850d8a0 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/basic.js @@ -0,0 +1,54 @@ +module.exports = { + basic: function (browser) { + browser + .url('http://localhost:8080/basic/') + .waitForElementVisible('#app', 1000) + .assert.count('li', 7) + .assert.count('li a', 7) + // assert correct href with base + .assert.attributeContains('li:nth-child(1) a', 'href', '/basic/') + .assert.attributeContains('li:nth-child(2) a', 'href', '/basic/foo') + .assert.attributeContains('li:nth-child(3) a', 'href', '/basic/bar') + .assert.attributeContains('li:nth-child(4) a', 'href', '/basic/bar') + .assert.attributeContains('li:nth-child(5) a', 'href', '/basic/%C3%A9') + .assert.containsText('.view', 'home') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/basic/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/basic/bar') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/basic/') + .assert.containsText('.view', 'home') + + .click('li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/basic/bar') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/basic/%C3%A9') + .assert.containsText('.view', 'unicode') + + // check initial visit + .url('http://localhost:8080/basic/foo') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'foo') + .url('http://localhost:8080/basic/%C3%A9') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'unicode') + + // regression onComplete + // https://github.com/vuejs/vue-router/issues/2721 + .assert.containsText('#counter', '0') + .click('#navigate-btn') + .assert.containsText('#counter', '1') + .click('#navigate-btn') + .assert.containsText('#counter', '2') + + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/data-fetching.js b/2.Vue-router/test/e2e/specs/data-fetching.js new file mode 100644 index 0000000..f44ea6b --- /dev/null +++ b/2.Vue-router/test/e2e/specs/data-fetching.js @@ -0,0 +1,29 @@ +module.exports = { + 'data fetching': function (browser) { + browser + .url('http://localhost:8080/data-fetching/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 4) + .assert.containsText('.view', 'home') + + .click('li:nth-child(2) a') + .waitForElementNotPresent('.loading', 500) + .assert.containsText('.post h2', 'sunt aut facere') + .assert.containsText('.post p', 'quia et suscipit') + + .click('li:nth-child(3) a') + .waitForElementNotPresent('.loading', 500) + .assert.containsText('.post h2', 'qui est esse') + .assert.containsText('.post p', 'est rerum tempore') + + .click('li:nth-child(4) a') + .waitForElementNotPresent('.loading', 500) + .assert.elementNotPresent('.content') + .assert.containsText('.error', 'Post not found') + + .click('li:nth-child(1) a') + .assert.elementNotPresent('.post') + .assert.containsText('.view', 'home') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/hash-mode.js b/2.Vue-router/test/e2e/specs/hash-mode.js new file mode 100644 index 0000000..554a17e --- /dev/null +++ b/2.Vue-router/test/e2e/specs/hash-mode.js @@ -0,0 +1,56 @@ +module.exports = { + 'Hash mode': function (browser) { + browser + .url('http://localhost:8080/hash-mode/') + .waitForElementVisible('#app', 1000) + .assert.count('li', 8) + .assert.count('li a', 7) + .assert.attributeContains('li:nth-child(1) a', 'href', '/hash-mode/#/') + .assert.attributeContains('li:nth-child(2) a', 'href', '/hash-mode/#/foo') + .assert.attributeContains('li:nth-child(3) a', 'href', '/hash-mode/#/bar') + .assert.attributeContains('li:nth-child(5) a', 'href', '/hash-mode/#/%C3%A9') + .assert.attributeContains('li:nth-child(6) a', 'href', '/hash-mode/#/%C3%A9/%C3%B1') + .assert.attributeContains('li:nth-child(7) a', 'href', '/hash-mode/#/%C3%A9/%C3%B1?t=%25%C3%B1') + .assert.containsText('.view', 'home') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/hash-mode/#/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/hash-mode/#/bar') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/hash-mode/#/') + .assert.containsText('.view', 'home') + + .click('li:nth-child(4)') + .assert.urlEquals('http://localhost:8080/hash-mode/#/bar') + .assert.containsText('.view', 'bar') + + // check initial visit + .url('http://localhost:8080/hash-mode/#/foo') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'foo') + // direct visit encoded unicode + .url('http://localhost:8080/hash-mode/#/%C3%A9') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'unicode') + // direct visit raw unicode + .url('http://localhost:8080/hash-mode/#/%C3%A9/%C3%B1') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'unicode: ñ') + // TODO: Doesn't seem to work on PhantomJS + // .click('li:nth-child(7)') + // .assert.urlEquals('http://localhost:8080/hash-mode/#/%C3%A9/%C3%B1?t=%25') + // .assert.containsText('.view', 'unicode: ñ') + // .assert.containsText('#query-t', '%') + // direct visit + .url('http://localhost:8080/hash-mode/#/%C3%A9/%C3%B1?t=%25') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'unicode: ñ') + .assert.containsText('#query-t', '%') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/hash-scroll-behavior.js b/2.Vue-router/test/e2e/specs/hash-scroll-behavior.js new file mode 100644 index 0000000..8fe16ed --- /dev/null +++ b/2.Vue-router/test/e2e/specs/hash-scroll-behavior.js @@ -0,0 +1,61 @@ +module.exports = { + 'scroll behavior': function (browser) { + browser + .url('http://localhost:8080/hash-scroll-behavior/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 5) + .assert.containsText('.view', 'home') + + .execute(function () { + window.scrollTo(0, 100) + }) + .click('li:nth-child(2) a') + .assert.containsText('.view', 'foo') + .execute(function () { + window.scrollTo(0, 200) + window.history.back() + }) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 100 + }, null, 'restore scroll position on back') + + // scroll on a popped entry + .execute(function () { + window.scrollTo(0, 50) + window.history.forward() + }) + .assert.containsText('.view', 'foo') + .assert.evaluate(function () { + return window.pageYOffset === 200 + }, null, 'restore scroll position on forward') + + .execute(function () { + window.history.back() + }) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 50 + }, null, 'restore scroll position on back again') + + .click('li:nth-child(3) a') + .assert.evaluate(function () { + return window.pageYOffset === 0 + }, null, 'scroll to top on new entry') + + .click('li:nth-child(4) a') + .assert.evaluate(function () { + return document.getElementById('anchor').getBoundingClientRect().top < 1 + }, null, 'scroll to anchor') + + // scroll back to top so we can click the butotn + .execute(function () { + window.scrollTo(0, 0) + }) + .click('li:nth-child(5) a') + .assert.evaluate(function () { + return document.getElementById('anchor2').getBoundingClientRect().top < 101 + }, null, 'scroll to anchor with offset') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/keepalive-view.js b/2.Vue-router/test/e2e/specs/keepalive-view.js new file mode 100644 index 0000000..bb8a67f --- /dev/null +++ b/2.Vue-router/test/e2e/specs/keepalive-view.js @@ -0,0 +1,34 @@ +module.exports = { + 'keepalive view': function (browser) { + browser + .url('http://localhost:8080/keepalive-view/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 5) + + .click('li:nth-child(1) a') + .assert.containsText('.view', 'index child1') + + .click('li:nth-child(2) a') + .assert.containsText('.view', 'index child2') + + .click('li:nth-child(3) a') + .assert.containsText('.view', 'home') + + // back to index child1 and check it + .click('li:nth-child(1) a') + .assert.containsText('.view', 'index child1') + + // beforeRouteEnter guard with keep alive + // https://github.com/vuejs/vue-router/issues/2561 + .click('li:nth-child(4) a') + .assert.containsText('.view', 'with-guard1: 1') + .click('li:nth-child(3) a') + .assert.containsText('.view', 'home') + .click('li:nth-child(5) a') + .assert.containsText('.view', 'with-guard2: 2') + .click('li:nth-child(4) a') + .assert.containsText('.view', 'with-guard1: 3') + + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/lazy-loading-before-mount.js b/2.Vue-router/test/e2e/specs/lazy-loading-before-mount.js new file mode 100644 index 0000000..444dff3 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/lazy-loading-before-mount.js @@ -0,0 +1,11 @@ +module.exports = { + 'lazy loading before mount': function (browser) { + browser + .url('http://localhost:8080/lazy-loading-before-mount/') + // wait for the Foo component to be resolved + .click('#load-button') + .waitForElementVisible('.foo', 1000) + .assert.containsText('.view', 'This is Foo') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/lazy-loading.js b/2.Vue-router/test/e2e/specs/lazy-loading.js new file mode 100644 index 0000000..8fdb788 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/lazy-loading.js @@ -0,0 +1,45 @@ +module.exports = { + 'lazy loading': function (browser) { + browser + .url('http://localhost:8080/lazy-loading/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 5) + .assert.containsText('.view', 'home') + + .click('li:nth-child(2) a') + .assert.containsText('.view', 'This is Foo!') + + .click('li:nth-child(3) a') + .assert.containsText('.view', 'This is Bar!') + + .click('li:nth-child(1) a') + .assert.containsText('.view', 'home') + + .click('li:nth-child(4) a') + .assert.containsText('.view', 'This is Bar!') + .assert.containsText('.view h3', 'Baz') + + // test initial visit + .url('http://localhost:8080/lazy-loading/foo') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'This is Foo!') + + .url('http://localhost:8080/lazy-loading/bar/baz') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'This is Bar!') + .assert.containsText('.view h3', 'Baz') + + // lazy loading with dynamic params: https://github.com/vuejs/vue-router/issues/2719 + // direct visit + .url('http://localhost:8080/lazy-loading/a/b/c') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', '/a/b/c') + // coming from another url + .url('http://localhost:8080/lazy-loading/') + .waitForElementVisible('#app', 1000) + .click('li:nth-child(5) a') + .waitForElementVisible('#tagged-path', 1000) + .assert.containsText('.view', '/a/b/c') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/named-routes.js b/2.Vue-router/test/e2e/specs/named-routes.js new file mode 100644 index 0000000..f6c0e9c --- /dev/null +++ b/2.Vue-router/test/e2e/specs/named-routes.js @@ -0,0 +1,36 @@ +module.exports = { + 'named routes': function (browser) { + browser + .url('http://localhost:8080/named-routes/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 3) + // assert correct href with base + .assert.attributeContains('li:nth-child(1) a', 'href', '/named-routes/') + .assert.attributeContains('li:nth-child(2) a', 'href', '/named-routes/foo') + .assert.attributeContains('li:nth-child(3) a', 'href', '/named-routes/bar') + .assert.containsText('p', 'Current route name: home') + .assert.containsText('.view', 'Home') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/named-routes/foo') + .assert.containsText('p', 'Current route name: foo') + .assert.containsText('.view', 'Foo') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/named-routes/bar/123') + .assert.containsText('p', 'Current route name: bar') + .assert.containsText('.view', 'Bar 123') + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/named-routes/') + .assert.containsText('p', 'Current route name: home') + .assert.containsText('.view', 'Home') + + // check initial visit + .url('http://localhost:8080/named-routes/foo') + .waitForElementVisible('#app', 1000) + .assert.containsText('p', 'Current route name: foo') + .assert.containsText('.view', 'Foo') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/named-views.js b/2.Vue-router/test/e2e/specs/named-views.js new file mode 100644 index 0000000..9634528 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/named-views.js @@ -0,0 +1,35 @@ +module.exports = { + 'named views': function (browser) { + browser + .url('http://localhost:8080/named-views/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 2) + // assert correct href with base + .assert.attributeContains('li:nth-child(1) a', 'href', '/named-views/') + .assert.attributeContains('li:nth-child(2) a', 'href', '/named-views/other') + + .assert.containsText('.view.one', 'foo') + .assert.containsText('.view.two', 'bar') + .assert.containsText('.view.three', 'baz') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/named-views/other') + .assert.containsText('.view.one', 'baz') + .assert.containsText('.view.two', 'bar') + .assert.containsText('.view.three', 'foo') + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/named-views/') + .assert.containsText('.view.one', 'foo') + .assert.containsText('.view.two', 'bar') + .assert.containsText('.view.three', 'baz') + + // check initial visit + .url('http://localhost:8080/named-views/other') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view.one', 'baz') + .assert.containsText('.view.two', 'bar') + .assert.containsText('.view.three', 'foo') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/navigation-guards.js b/2.Vue-router/test/e2e/specs/navigation-guards.js new file mode 100644 index 0000000..4b75c5a --- /dev/null +++ b/2.Vue-router/test/e2e/specs/navigation-guards.js @@ -0,0 +1,135 @@ +module.exports = { + 'navigation guards': function (browser) { + // alert commands not available in phantom + if (process.env.PHANTOMJS) { + return + } + + browser + .url('http://localhost:8080/navigation-guards/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 8) + .assert.containsText('.view', 'home') + + .click('li:nth-child(2) a') + .dismissAlert() + .waitFor(100) + .dismissAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/') + .assert.containsText('.view', 'home') + + .click('li:nth-child(2) a') + .acceptAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .dismissAlert() + .waitFor(100) + .dismissAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .acceptAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/bar') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(2) a') + .dismissAlert() + .waitFor(100) + .acceptAlert() // redirect to baz + .assert.urlEquals('http://localhost:8080/navigation-guards/baz') + .assert.containsText('.view', 'baz (not saved)') + + .click('li:nth-child(2) a') + .dismissAlert() // not saved + .assert.urlEquals('http://localhost:8080/navigation-guards/baz') + .assert.containsText('.view', 'baz (not saved)') + + .click('li:nth-child(2) a') + .acceptAlert() // not saved, force leave + .waitFor(100) + .dismissAlert() // should trigger foo's guard + .waitFor(100) + .dismissAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/baz') + .assert.containsText('.view', 'baz') + + .click('li:nth-child(2) a') + .acceptAlert() + .waitFor(100) + .acceptAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/baz') + .assert.containsText('.view', 'baz (not saved)') + .click('button') + .assert.containsText('.view', 'baz (saved)') + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/') + .assert.containsText('.view', 'home') + + // test initial visit + .url('http://localhost:8080/navigation-guards/foo') + .dismissAlert() + .waitFor(100) + .dismissAlert() + // should redirect to root + .assert.urlEquals('http://localhost:8080/navigation-guards/') + // and should not render anything + .assert.elementNotPresent('.view') + + .url('http://localhost:8080/navigation-guards/foo') + .acceptAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/foo') + .assert.containsText('.view', 'foo') + + .url('http://localhost:8080/navigation-guards/bar') + .dismissAlert() + .waitFor(100) + .dismissAlert() + // should redirect to root + .assert.urlEquals('http://localhost:8080/navigation-guards/') + // and should not render anything + .assert.elementNotPresent('.view') + + .url('http://localhost:8080/navigation-guards/bar') + .acceptAlert() + .assert.urlEquals('http://localhost:8080/navigation-guards/bar') + .assert.containsText('.view', 'bar') + + // in-component guard + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/bar') + .assert.containsText('.view', 'bar') + .waitFor(300) + .assert.urlEquals('http://localhost:8080/navigation-guards/qux') + .assert.containsText('.view', 'Qux') + + // async component + in-component guard + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/') + .assert.containsText('.view', 'home') + .click('li:nth-child(6) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/') + .assert.containsText('.view', 'home') + .waitFor(300) + .assert.urlEquals('http://localhost:8080/navigation-guards/qux-async') + .assert.containsText('.view', 'Qux') + + // beforeRouteUpdate + .click('li:nth-child(7) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/quux/1') + .assert.containsText('.view', 'id:1 prevId:0') + .click('li:nth-child(8) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/quux/2') + .assert.containsText('.view', 'id:2 prevId:1') + .click('li:nth-child(7) a') + .assert.urlEquals('http://localhost:8080/navigation-guards/quux/1') + .assert.containsText('.view', 'id:1 prevId:2') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/nested-router.js b/2.Vue-router/test/e2e/specs/nested-router.js new file mode 100644 index 0000000..dd07fce --- /dev/null +++ b/2.Vue-router/test/e2e/specs/nested-router.js @@ -0,0 +1,28 @@ +module.exports = { + 'basic': function (browser) { + browser + .url('http://localhost:8080/nested-router/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 3) + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/nested-router/nested-router') + .assert.containsText('.child', 'Child router path: /') + .assert.count('li a', 5) + + .click('.child li:nth-child(1) a') + .assert.containsText('.child', 'Child router path: /foo') + .assert.containsText('.child .foo', 'foo') + + .click('.child li:nth-child(2) a') + .assert.containsText('.child', 'Child router path: /bar') + .assert.containsText('.child .bar', 'bar') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/nested-router/foo') + .assert.elementNotPresent('.child') + .assert.containsText('#app', 'foo') + .assert.count('li a', 3) + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/nested-routes.js b/2.Vue-router/test/e2e/specs/nested-routes.js new file mode 100644 index 0000000..ef52a25 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/nested-routes.js @@ -0,0 +1,91 @@ +module.exports = { + 'nested routes': function (browser) { + browser + .url('http://localhost:8080/nested-routes/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 11) + .assert.urlEquals('http://localhost:8080/nested-routes/parent') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'default') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/foo') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/bar') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/nested-routes/baz') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'baz') + + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/qux/123') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'qux') + + .click('.nested-parent a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/qux/123/quux') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'qux') + .assert.containsText('.view', 'quux') + + .click('li:nth-child(6) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/quy/123') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'quy') + .assert.evaluate(function () { + var params = JSON.parse(document.querySelector('pre').textContent) + return ( + JSON.stringify(params) === JSON.stringify(['quyId']) + ) + }, null, 'quyId') + + .click('li:nth-child(8) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/zap/1') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'zap') + .assert.evaluate(function () { + var zapId = document.querySelector('pre').textContent + return (zapId === '1') + }, null, 'zapId') + + .click('li:nth-child(7) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/zap') + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'zap') + .assert.evaluate(function () { + var zapId = document.querySelector('pre').textContent + return (zapId === '') + }, null, 'optional zapId') + + // test relative params + .click('li:nth-child(9) a') + .assert.evaluate(function () { + var zapId = document.querySelector('pre').textContent + return (zapId === '2') + }, null, 'relative params') + + .click('li:nth-child(10) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/qux/1/quux') + .click('li:nth-child(11) a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/qux/2/quux') + .click('.nested-child a') + .assert.urlEquals('http://localhost:8080/nested-routes/parent/qux/2/quuy') + + // check initial visit + .url('http://localhost:8080/nested-routes/parent/foo') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'foo') + .url('http://localhost:8080/nested-routes/baz') + .waitForElementVisible('#app', 1000) + .assert.containsText('.view', 'Parent') + .assert.containsText('.view', 'baz') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/redirect.js b/2.Vue-router/test/e2e/specs/redirect.js new file mode 100644 index 0000000..3ce8796 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/redirect.js @@ -0,0 +1,133 @@ +module.exports = { + 'redirect': function (browser) { + browser + .url('http://localhost:8080/redirect/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 12) + // assert correct href with base + .assert.attributeContains('li:nth-child(1) a', 'href', '/redirect/relative-redirect') + .assert.attributeContains('li:nth-child(2) a', 'href', '/redirect/relative-redirect?foo=bar') + .assert.attributeContains('li:nth-child(3) a', 'href', '/redirect/absolute-redirect') + .assert.attributeContains('li:nth-child(4) a', 'href', '/redirect/dynamic-redirect') + .assert.attributeContains('li:nth-child(5) a', 'href', '/redirect/dynamic-redirect/123') + .assert.attributeContains('li:nth-child(6) a', 'href', '/redirect/dynamic-redirect?to=foo') + .assert.attributeContains('li:nth-child(7) a', 'href', '/redirect/dynamic-redirect#baz') + .assert.attributeContains('li:nth-child(8) a', 'href', '/redirect/named-redirect') + .assert.attributeContains('li:nth-child(9) a', 'href', '/redirect/redirect-with-params/123') + .assert.attributeContains('li:nth-child(10) a', 'href', '/redirect/foobar') + .assert.attributeContains('li:nth-child(11) a', 'href', '/redirect/FooBar') + .assert.attributeContains('li:nth-child(12) a', 'href', '/not-found') + + .assert.containsText('.view', 'default') + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/redirect/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/redirect/foo?foo=bar') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/redirect/bar') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/redirect/bar') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/redirect/with-params/123') + .assert.containsText('.view', '123') + + .click('li:nth-child(6) a') + .assert.urlEquals('http://localhost:8080/redirect/foo') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(7) a') + .assert.urlEquals('http://localhost:8080/redirect/baz') + .assert.containsText('.view', 'baz') + + .click('li:nth-child(8) a') + .assert.urlEquals('http://localhost:8080/redirect/baz') + .assert.containsText('.view', 'baz') + + .click('li:nth-child(9) a') + .assert.urlEquals('http://localhost:8080/redirect/with-params/123') + .assert.containsText('.view', '123') + + .click('li:nth-child(10) a') + .assert.urlEquals('http://localhost:8080/redirect/foobar') + .assert.containsText('.view', 'foobar') + + .click('li:nth-child(11) a') + .assert.urlEquals('http://localhost:8080/redirect/FooBar') + .assert.containsText('.view', 'FooBar') + + .click('li:nth-child(12) a') + .assert.urlEquals('http://localhost:8080/redirect/') + .assert.containsText('.view', 'default') + + // check initial visit + .url('http://localhost:8080/redirect/relative-redirect') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/foo') + .assert.containsText('.view', 'foo') + + .url('http://localhost:8080/redirect/relative-redirect?foo=bar') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/foo?foo=bar') + .assert.containsText('.view', 'foo') + + .url('http://localhost:8080/redirect/absolute-redirect') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/bar') + .assert.containsText('.view', 'bar') + + .url('http://localhost:8080/redirect/dynamic-redirect') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/bar') + .assert.containsText('.view', 'bar') + + .url('http://localhost:8080/redirect/dynamic-redirect/123') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/with-params/123') + .assert.containsText('.view', '123') + + .url('http://localhost:8080/redirect/dynamic-redirect?to=foo') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/foo') + .assert.containsText('.view', 'foo') + + .url('http://localhost:8080/redirect/dynamic-redirect#baz') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/baz') + .assert.containsText('.view', 'baz') + + .url('http://localhost:8080/redirect/named-redirect') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/baz') + .assert.containsText('.view', 'baz') + + .url('http://localhost:8080/redirect/redirect-with-params/123') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/with-params/123') + .assert.containsText('.view', '123') + + .url('http://localhost:8080/redirect/foobar') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/foobar') + .assert.containsText('.view', 'foobar') + + .url('http://localhost:8080/redirect/FooBar') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/FooBar') + .assert.containsText('.view', 'FooBar') + + .url('http://localhost:8080/redirect/not-found') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/redirect/') + .assert.containsText('.view', 'default') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/route-alias.js b/2.Vue-router/test/e2e/specs/route-alias.js new file mode 100644 index 0000000..2193bce --- /dev/null +++ b/2.Vue-router/test/e2e/specs/route-alias.js @@ -0,0 +1,88 @@ +module.exports = { + 'route alias': function (browser) { + browser + .url('http://localhost:8080/route-alias/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 7) + // assert correct href with base + .assert.attributeContains('li:nth-child(1) a', 'href', '/root-alias') + .assert.attributeContains('li:nth-child(2) a', 'href', '/route-alias/foo') + .assert.attributeContains('li:nth-child(3) a', 'href', '/route-alias/home/bar-alias') + .assert.attributeContains('li:nth-child(4) a', 'href', '/route-alias/baz') + .assert.attributeContains('li:nth-child(5) a', 'href', '/route-alias/home/baz-alias') + .assert.attributeEquals('li:nth-child(6) a', 'href', 'http://localhost:8080/route-alias/home') + .assert.attributeContains('li:nth-child(7) a', 'href', '/route-alias/home/nested-alias/foo') + + .click('li:nth-child(1) a') + .assert.urlEquals('http://localhost:8080/route-alias/root-alias') + .assert.containsText('.view', 'root') + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/route-alias/foo') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'foo') + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/route-alias/home/bar-alias') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'bar') + + .click('li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/route-alias/baz') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'baz') + + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/route-alias/home/baz-alias') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'baz') + + .click('li:nth-child(6) a') + .assert.urlEquals('http://localhost:8080/route-alias/home') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'default') + + .click('li:nth-child(7) a') + .assert.urlEquals('http://localhost:8080/route-alias/home/nested-alias/foo') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'nested foo') + + // check initial visit + .url('http://localhost:8080/route-alias/foo') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/route-alias/foo') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'foo') + + .url('http://localhost:8080/route-alias/home/bar-alias') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/route-alias/home/bar-alias') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'bar') + + .url('http://localhost:8080/route-alias/baz') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/route-alias/baz') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'baz') + + .url('http://localhost:8080/route-alias/home/baz-alias') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/route-alias/home/baz-alias') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'baz') + + .url('http://localhost:8080/route-alias/home') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/route-alias/home') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'default') + + .url('http://localhost:8080/route-alias/home/nested-alias/foo') + .waitForElementVisible('#app', 1000) + .assert.urlEquals('http://localhost:8080/route-alias/home/nested-alias/foo') + .assert.containsText('.view', 'Home') + .assert.containsText('.view', 'nested foo') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/route-matching.js b/2.Vue-router/test/e2e/specs/route-matching.js new file mode 100644 index 0000000..c83070f --- /dev/null +++ b/2.Vue-router/test/e2e/specs/route-matching.js @@ -0,0 +1,129 @@ +module.exports = { + 'route-matching': function (browser) { + browser + .url('http://localhost:8080/route-matching/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 10) + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '' && + route.fullPath === '/' && + JSON.stringify(route.params) === JSON.stringify({}) + ) + }, null, '/') + + .click('li:nth-child(2) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/params/:foo/:bar' && + route.fullPath === '/params/foo/bar' && + JSON.stringify(route.params) === JSON.stringify({ + foo: 'foo', + bar: 'bar' + }) + ) + }, null, '/params/foo/bar') + + .click('li:nth-child(3) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/optional-params/:foo?' && + route.fullPath === '/optional-params' && + JSON.stringify(route.params) === JSON.stringify({}) + ) + }, null, '/optional-params') + + .click('li:nth-child(4) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/optional-params/:foo?' && + route.fullPath === '/optional-params/foo' && + JSON.stringify(route.params) === JSON.stringify({ + foo: 'foo' + }) + ) + }, null, '/optional-params/foo') + + .click('li:nth-child(5) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/params-with-regex/:id(\\d+)' && + route.fullPath === '/params-with-regex/123' && + JSON.stringify(route.params) === JSON.stringify({ + id: '123' + }) + ) + }, null, '/params-with-regex/123') + + .click('li:nth-child(6) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 0 && + route.fullPath === '/params-with-regex/abc' && + JSON.stringify(route.params) === JSON.stringify({}) + ) + }, null, '/params-with-regex/abc') + + .click('li:nth-child(7) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/asterisk/*' && + route.fullPath === '/asterisk/foo' && + JSON.stringify(route.params) === JSON.stringify({ + pathMatch: 'foo' + }) + ) + }, null, '/asterisk/foo') + + .click('li:nth-child(8) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/asterisk/*' && + route.fullPath === '/asterisk/foo/bar' && + JSON.stringify(route.params) === JSON.stringify({ + pathMatch: 'foo/bar' + }) + ) + }, null, '/asterisk/foo/bar') + + .click('li:nth-child(9) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/optional-group/(foo/)?bar' && + route.fullPath === '/optional-group/bar' && + JSON.stringify(route.params) === JSON.stringify({}) + ) + }, null, '/optional-group/bar') + + .click('li:nth-child(10) a') + .assert.evaluate(function () { + var route = JSON.parse(document.querySelector('pre').textContent) + return ( + route.matched.length === 1 && + route.matched[0].path === '/optional-group/(foo/)?bar' && + route.fullPath === '/optional-group/foo/bar' && + JSON.stringify(route.params) === JSON.stringify({ + pathMatch: 'foo/' + }) + ) + }, null, '/optional-group/foo/bar') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/route-props.js b/2.Vue-router/test/e2e/specs/route-props.js new file mode 100644 index 0000000..b6f6193 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/route-props.js @@ -0,0 +1,37 @@ +const $attrs = ' { "foo": "123" }' + +module.exports = { + 'route-props': function (browser) { + browser + .url('http://localhost:8080/route-props/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 5) + + .assert.urlEquals('http://localhost:8080/route-props/') + .assert.containsText('.hello', 'Hello Vue!' + $attrs) + + .click('li:nth-child(2) a') + .assert.urlEquals('http://localhost:8080/route-props/hello/you') + .assert.containsText('.hello', 'Hello you' + $attrs) + + .click('li:nth-child(3) a') + .assert.urlEquals('http://localhost:8080/route-props/static') + .assert.containsText('.hello', 'Hello world' + $attrs) + + .click('li:nth-child(4) a') + .assert.urlEquals('http://localhost:8080/route-props/dynamic/1') + .assert.containsText('.hello', 'Hello ' + ((new Date()).getFullYear() + 1) + '!' + $attrs) + + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/route-props/attrs') + .assert.containsText('.hello', 'Hello attrs' + $attrs) + + // should be consistent + .click('li:nth-child(4) a') + .click('li:nth-child(5) a') + .assert.urlEquals('http://localhost:8080/route-props/attrs') + .assert.containsText('.hello', 'Hello attrs' + $attrs) + + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/scroll-behavior.js b/2.Vue-router/test/e2e/specs/scroll-behavior.js new file mode 100644 index 0000000..5e88ef5 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/scroll-behavior.js @@ -0,0 +1,88 @@ +module.exports = { + 'scroll behavior': function (browser) { + const TIMEOUT = 2000 + + browser + .url('http://localhost:8080/scroll-behavior/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 5) + .assert.containsText('.view', 'home') + + .execute(function () { + window.scrollTo(0, 100) + }) + .click('li:nth-child(2) a') + .waitForElementPresent('.view.foo', TIMEOUT) + .assert.containsText('.view', 'foo') + .execute(function () { + window.scrollTo(0, 200) + window.history.back() + }) + .waitForElementPresent('.view.home', TIMEOUT) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 100 + }, null, 'restore scroll position on back') + + // with manual scroll restoration + // https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration + .execute(function () { + window.scrollTo(0, 100) + history.scrollRestoration = 'manual' + }) + .click('li:nth-child(2) a') + .waitForElementPresent('.view.foo', TIMEOUT) + .assert.containsText('.view', 'foo') + .execute(function () { + window.scrollTo(0, 200) + window.history.back() + }) + .waitForElementPresent('.view.home', TIMEOUT) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 100 + }, null, 'restore scroll position on back with manual restoration') + .execute(function () { + history.scrollRestoration = 'auto' + }) + + // scroll on a popped entry + .execute(function () { + window.scrollTo(0, 50) + window.history.forward() + }) + .waitForElementPresent('.view.foo', TIMEOUT) + .assert.containsText('.view', 'foo') + .assert.evaluate(function () { + return window.pageYOffset === 200 + }, null, 'restore scroll position on forward') + + .execute(function () { + window.history.back() + }) + .waitForElementPresent('.view.home', TIMEOUT) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 50 + }, null, 'restore scroll position on back again') + + .click('li:nth-child(3) a') + .waitForElementPresent('.view.bar', TIMEOUT) + .assert.evaluate(function () { + return window.pageYOffset === 0 + }, null, 'scroll to top on new entry') + + .click('li:nth-child(4) a') + .assert.evaluate(function () { + return document.getElementById('anchor').getBoundingClientRect().top < 1 + }, null, 'scroll to anchor') + + .execute(function () { + document.querySelector('li:nth-child(5) a').click() + }) + .assert.evaluate(function () { + return document.getElementById('anchor2').getBoundingClientRect().top < 101 + }, null, 'scroll to anchor with offset') + .end() + } +} diff --git a/2.Vue-router/test/e2e/specs/transitions.js b/2.Vue-router/test/e2e/specs/transitions.js new file mode 100644 index 0000000..4c21a39 --- /dev/null +++ b/2.Vue-router/test/e2e/specs/transitions.js @@ -0,0 +1,40 @@ +module.exports = { + 'transitions': function (browser) { + const TIMEOUT = 2000 + + browser + .url('http://localhost:8080/transitions/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 4) + + .click('li:nth-child(2) a') + .assert.cssClassPresent('.view.home', 'fade-leave-active') + .waitForElementPresent('.view.parent', TIMEOUT) + .assert.cssClassPresent('.view.parent', 'fade-enter-active') + .assert.cssClassNotPresent('.child-view.default', 'slide-left-enter-active') + .waitForElementNotPresent('.view.parent.fade-enter-active', TIMEOUT) + + .click('li:nth-child(3) a') + .assert.cssClassPresent('.child-view.default', 'slide-left-leave-active') + .assert.cssClassPresent('.child-view.foo', 'slide-left-enter-active') + .waitForElementNotPresent('.child-view.default', TIMEOUT) + + .click('li:nth-child(4) a') + .assert.cssClassPresent('.child-view.foo', 'slide-left-leave-active') + .assert.cssClassPresent('.child-view.bar', 'slide-left-enter-active') + .waitForElementNotPresent('.child-view.foo', TIMEOUT) + + .click('li:nth-child(2) a') + .assert.cssClassPresent('.child-view.bar', 'slide-right-leave-active') + .assert.cssClassPresent('.child-view.default', 'slide-right-enter-active') + .waitForElementNotPresent('.child-view.bar', TIMEOUT) + + .click('li:nth-child(1) a') + .assert.cssClassPresent('.view.parent', 'fade-leave-active') + .waitForElementPresent('.view.home', TIMEOUT) + .assert.cssClassPresent('.view.home', 'fade-enter-active') + .waitForElementNotPresent('.view.home.fade-enter-active', TIMEOUT) + + .end() + } +} diff --git a/2.Vue-router/test/unit/jasmine.json b/2.Vue-router/test/unit/jasmine.json new file mode 100644 index 0000000..d437c4e --- /dev/null +++ b/2.Vue-router/test/unit/jasmine.json @@ -0,0 +1,9 @@ +{ + "spec_dir": "test/unit/specs", + "spec_files": [ + "*.spec.js" + ], + "helpers": [ + "../../../node_modules/babel-register/lib/node.js" + ] +} diff --git a/2.Vue-router/test/unit/specs/api.spec.js b/2.Vue-router/test/unit/specs/api.spec.js new file mode 100644 index 0000000..1fba497 --- /dev/null +++ b/2.Vue-router/test/unit/specs/api.spec.js @@ -0,0 +1,282 @@ +import Router from '../../../src/index' +import Vue from 'vue' + +describe('router.onReady', () => { + it('should work', done => { + const calls = [] + + const router = new Router({ + mode: 'abstract', + routes: [ + { + path: '/a', + component: { + name: 'A', + beforeRouteEnter: (to, from, next) => { + setTimeout(() => { + calls.push(2) + next() + }, 1) + } + } + } + ] + }) + + router.beforeEach((to, from, next) => { + setTimeout(() => { + calls.push(1) + next() + }, 1) + }) + + router.onReady(() => { + expect(calls).toEqual([1, 2]) + // sync call when already ready + router.onReady(() => { + calls.push(3) + }) + expect(calls).toEqual([1, 2, 3]) + done() + }) + + router.push('/a') + expect(calls).toEqual([]) + }) +}) + +describe('route matching', () => { + it('resolves parent params when using current route', () => { + const router = new Router({ + mode: 'abstract', + routes: [ + { + path: '/a/:id', + component: { name: 'A' }, + children: [{ name: 'b', path: 'b', component: { name: 'B' }}] + } + ] + }) + + router.push('/a/1') + + const { route, resolved } = router.resolve({ name: 'b' }) + expect(route.params).toEqual({ id: '1' }) + expect(resolved.params).toEqual({ id: '1' }) + }) + + it('can override currentRoute', () => { + const router = new Router({ + mode: 'abstract', + routes: [ + { + path: '/a/:id', + component: { name: 'A' }, + children: [{ name: 'b', path: 'b', component: { name: 'B' }}] + } + ] + }) + + router.push('/a/1') + + const { route, resolved } = router.resolve({ name: 'b' }, { params: { id: '2' }, path: '/a/2' }) + expect(route.params).toEqual({ id: '2' }) + expect(resolved.params).toEqual({ id: '2' }) + }) +}) + +describe('router.addRoutes', () => { + it('should work', () => { + const router = new Router({ + mode: 'abstract', + routes: [ + { path: '/a', component: { name: 'A' }} + ] + }) + + router.push('/a') + let components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('A') + + router.push('/b') + components = router.getMatchedComponents() + expect(components.length).toBe(0) + + router.addRoutes([ + { path: '/b', component: { name: 'B' }} + ]) + components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('B') + + // make sure it preserves previous routes + router.push('/a') + components = router.getMatchedComponents() + expect(components.length).toBe(1) + expect(components[0].name).toBe('A') + }) +}) + +describe('router.push/replace callbacks', () => { + let calls = [] + let router, spy1, spy2 + + const Foo = { + beforeRouteEnter (to, from, next) { + calls.push(3) + setTimeout(() => { + calls.push(4) + next() + }, 1) + } + } + + beforeEach(() => { + calls = [] + spy1 = jasmine.createSpy('complete') + spy2 = jasmine.createSpy('abort') + + router = new Router({ + routes: [ + { path: '/foo', component: Foo } + ] + }) + + router.beforeEach((to, from, next) => { + calls.push(1) + setTimeout(() => { + calls.push(2) + next() + }, 1) + }) + }) + + it('push complete', done => { + router.push('/foo', () => { + expect(calls).toEqual([1, 2, 3, 4]) + done() + }) + }) + + it('push abort', done => { + router.push('/foo', spy1, spy2) + router.push('/bar', () => { + expect(calls).toEqual([1, 1, 2, 2]) + expect(spy1).not.toHaveBeenCalled() + expect(spy2).toHaveBeenCalled() + done() + }) + }) + + it('replace complete', done => { + router.replace('/foo', () => { + expect(calls).toEqual([1, 2, 3, 4]) + done() + }) + }) + + it('replace abort', done => { + router.replace('/foo', spy1, spy2) + router.replace('/bar', () => { + expect(calls).toEqual([1, 1, 2, 2]) + expect(spy1).not.toHaveBeenCalled() + expect(spy2).toHaveBeenCalled() + done() + }) + }) +}) + +describe('router app destroy handling', () => { + Vue.use(Router) + + let router, app1, app2, app3 + + beforeEach(() => { + router = new Router({ + mode: 'abstract', + routes: [ + { path: '/', component: { name: 'A' }} + ] + }) + + // Add main app + app1 = new Vue({ + router, + render (h) { return h('div') } + }) + + // Add 2nd app + app2 = new Vue({ + router, + render (h) { return h('div') } + }) + + // Add 3rd app + app3 = new Vue({ + router, + render (h) { return h('div') } + }) + }) + + it('all apps point to the same router instance', () => { + expect(app1.$router).toBe(app2.$router) + expect(app2.$router).toBe(app3.$router) + }) + + it('should have all 3 registered apps', () => { + expect(app1.$router.app).toBe(app1) + expect(app1.$router.apps.length).toBe(3) + expect(app1.$router.apps[0]).toBe(app1) + expect(app1.$router.apps[1]).toBe(app2) + expect(app1.$router.apps[2]).toBe(app3) + }) + + it('should remove 2nd destroyed app from this.apps', () => { + app2.$destroy() + expect(app1.$router.app).toBe(app1) + expect(app1.$router.apps.length).toBe(2) + expect(app1.$router.apps[0]).toBe(app1) + expect(app1.$router.apps[1]).toBe(app3) + }) + + it('should remove 1st destroyed app and replace current app', () => { + app1.$destroy() + expect(app3.$router.app).toBe(app2) + expect(app3.$router.apps.length).toBe(2) + expect(app3.$router.apps[0]).toBe(app2) + expect(app1.$router.apps[1]).toBe(app3) + }) + + it('should remove all apps', () => { + app1.$destroy() + app3.$destroy() + app2.$destroy() + expect(app3.$router.app).toBe(null) + expect(app3.$router.apps.length).toBe(0) + }) + + it('should keep current app if already defined', () => { + const app4 = new Vue({ + router, + render (h) { return h('div') } + }) + expect(app4.$router.app).toBe(app1) + expect(app4.$router.apps.length).toBe(4) + expect(app4.$router.apps[3]).toBe(app4) + }) + + it('should replace current app if none is assigned when creating the app', () => { + app1.$destroy() + app3.$destroy() + app2.$destroy() + const app4 = new Vue({ + router, + render (h) { return h('div') } + }) + expect(router.app).toBe(app4) + expect(app4.$router).toBe(router) + expect(app4.$router.apps.length).toBe(1) + expect(app4.$router.apps[0]).toBe(app4) + }) +}) diff --git a/2.Vue-router/test/unit/specs/async.spec.js b/2.Vue-router/test/unit/specs/async.spec.js new file mode 100644 index 0000000..4cd82be --- /dev/null +++ b/2.Vue-router/test/unit/specs/async.spec.js @@ -0,0 +1,17 @@ +import { runQueue } from '../../../src/util/async' + +describe('Async utils', () => { + describe('runQueue', () => { + it('should work', done => { + const calls = [] + const queue = [1, 2, 3, 4, 5].map(i => next => { + calls.push(i) + setTimeout(next, 0) + }) + runQueue(queue, (fn, next) => fn(next), () => { + expect(calls).toEqual([1, 2, 3, 4, 5]) + done() + }) + }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/create-map.spec.js b/2.Vue-router/test/unit/specs/create-map.spec.js new file mode 100644 index 0000000..67fba13 --- /dev/null +++ b/2.Vue-router/test/unit/specs/create-map.spec.js @@ -0,0 +1,174 @@ +/*eslint-disable no-undef*/ +import { createRouteMap } from '../../../src/create-route-map' + +const Home = { template: '
This is Home
' } +const Foo = { template: '
This is Foo
' } +const FooBar = { template: '
This is FooBar
' } +const Foobar = { template: '
This is foobar
' } +const Bar = { template: '
This is Bar
' } +const Baz = { template: '
This is Baz
' } + +const routes = [ + { path: '/', name: 'home', component: Home }, + { path: '/foo', name: 'foo', component: Foo }, + { path: '*', name: 'wildcard', component: Baz }, + { + path: '/bar', + name: 'bar', + component: Bar, + children: [ + { + path: '', + component: Baz, + name: 'bar.baz' + } + ] + }, + { + path: '/bar-redirect', + name: 'bar-redirect', + redirect: { name: 'bar-redirect.baz' }, + component: Bar, + children: [ + { + path: '', + component: Baz, + name: 'bar-redirect.baz' + } + ] + } +] + +describe('Creating Route Map', function () { + let maps + + beforeAll(function () { + spyOn(console, 'warn') + maps = createRouteMap(routes) + }) + + beforeEach(function () { + console.warn.calls.reset() + process.env.NODE_ENV = 'production' + }) + + it('has a pathMap object for default subroute at /bar/', function () { + expect(maps.pathMap['/bar/']).not.toBeUndefined() + }) + + it('has a pathList which places wildcards at the end', () => { + expect(maps.pathList).toEqual(['', '/foo', '/bar/', '/bar', '/bar-redirect/', '/bar-redirect', '*']) + }) + + it('has a nameMap object for default subroute at \'bar.baz\'', function () { + expect(maps.nameMap['bar.baz']).not.toBeUndefined() + }) + + it('in development, has logged a warning concerning named route of parent and default subroute', function () { + process.env.NODE_ENV = 'development' + maps = createRouteMap(routes) + expect(console.warn).toHaveBeenCalledTimes(1) + expect(console.warn.calls.argsFor(0)[0]).toMatch('vue-router] Named Route \'bar\'') + }) + + it('in development, throws if path is missing', function () { + process.env.NODE_ENV = 'development' + expect(() => { + maps = createRouteMap([{ component: Bar }]) + }).toThrowError(/"path" is required/) + }) + + it('in production, it has not logged this warning', function () { + maps = createRouteMap(routes) + expect(console.warn).not.toHaveBeenCalled() + }) + + it('in development, warn duplicate param keys', () => { + process.env.NODE_ENV = 'development' + maps = createRouteMap([ + { + path: '/foo/:id', component: Foo, + children: [ + { path: 'bar/:id', component: Bar } + ] + } + ]) + expect(console.warn).toHaveBeenCalled() + expect(console.warn.calls.argsFor(0)[0]).toMatch('vue-router] Duplicate param keys in route with path: "/foo/:id/bar/:id"') + }) + + describe('path-to-regexp options', function () { + const routes = [ + { path: '/foo', name: 'foo', component: Foo }, + { path: '/bar', name: 'bar', component: Bar, caseSensitive: false }, + { path: '/FooBar', name: 'FooBar', component: FooBar, caseSensitive: true }, + { path: '/foobar', name: 'foobar', component: Foobar, caseSensitive: true } + ] + + it('caseSensitive option in route', function () { + const { nameMap } = createRouteMap(routes) + + expect(nameMap.FooBar.regex.ignoreCase).toBe(false) + expect(nameMap.bar.regex.ignoreCase).toBe(true) + expect(nameMap.foo.regex.ignoreCase).toBe(true) + }) + + it('pathToRegexpOptions option in route', function () { + const { nameMap } = createRouteMap([ + { + name: 'foo', + path: '/foo', + component: Foo, + pathToRegexpOptions: { + sensitive: true + } + }, + { + name: 'bar', + path: '/bar', + component: Bar, + pathToRegexpOptions: { + sensitive: false + } + } + ]) + + expect(nameMap.foo.regex.ignoreCase).toBe(false) + expect(nameMap.bar.regex.ignoreCase).toBe(true) + }) + + it('caseSensitive over pathToRegexpOptions in route', function () { + const { nameMap } = createRouteMap([ + { + name: 'foo', + path: '/foo', + component: Foo, + caseSensitive: true, + pathToRegexpOptions: { + sensitive: false + } + } + ]) + + expect(nameMap.foo.regex.ignoreCase).toBe(false) + }) + + it('keeps trailing slashes with strict mode', function () { + const { pathList } = createRouteMap([ + { + path: '/foo/', + component: Foo, + pathToRegexpOptions: { + strict: true + } + }, + { + path: '/bar/', + component: Foo + } + ]) + + expect(pathList).toEqual(['/foo/', '/bar']) + }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/create-matcher.spec.js b/2.Vue-router/test/unit/specs/create-matcher.spec.js new file mode 100644 index 0000000..88fb475 --- /dev/null +++ b/2.Vue-router/test/unit/specs/create-matcher.spec.js @@ -0,0 +1,78 @@ +/*eslint-disable no-undef*/ +import { createMatcher } from '../../../src/create-matcher' + +const routes = [ + { path: '/', name: 'home', component: { name: 'home' }}, + { path: '/foo', name: 'foo', component: { name: 'foo' }}, + { path: '/baz/:testparam', name: 'baz', component: { name: 'baz' }}, + { path: '/error/*', name: 'error', component: { name: 'error' }}, + { path: '*', props: true, name: 'notFound', component: { name: 'notFound' }} +] + +describe('Creating Matcher', function () { + let match + + beforeAll(function () { + spyOn(console, 'warn') + match = createMatcher(routes).match + }) + + beforeEach(function () { + console.warn.calls.reset() + process.env.NODE_ENV = 'production' + }) + + it('in development, has logged a warning if a named route does not exist', function () { + process.env.NODE_ENV = 'development' + const { name, matched } = match({ name: 'bar' }, routes[0]) + expect(matched.length).toBe(0) + expect(name).toBe('bar') + expect(console.warn).toHaveBeenCalled() + expect(console.warn.calls.argsFor(0)[0]).toMatch( + "Route with name 'bar' does not exist" + ) + }) + + it('in production, it has not logged this warning', function () { + match({ name: 'foo' }, routes[0]) + expect(console.warn).not.toHaveBeenCalled() + }) + + it('matches named route with params without warning', function () { + process.env.NODE_ENV = 'development' + const { name, path, params } = match({ + name: 'baz', + params: { testparam: 'testvalue' } + }) + expect(console.warn).not.toHaveBeenCalled() + expect(name).toEqual('baz') + expect(path).toEqual('/baz/testvalue') + expect(params).toEqual({ testparam: 'testvalue' }) + }) + + it('matches asterisk routes with a default param name without warning', function () { + process.env.NODE_ENV = 'development' + const { params } = match( + { name: 'notFound', params: { pathMatch: '/not-found' }}, + routes[0] + ) + expect(console.warn).not.toHaveBeenCalled() + expect(params).toEqual({ pathMatch: '/not-found' }) + }) + + it('matches partial asterisk routes with a default param name without warning', function () { + process.env.NODE_ENV = 'development' + const { params, path } = match( + { name: 'error', params: { pathMatch: 'some' }}, + routes[0] + ) + expect(console.warn).not.toHaveBeenCalled() + expect(params).toEqual({ pathMatch: 'some' }) + expect(path).toEqual('/error/some') + }) + + it('matches asterisk routes with a default param name', function () { + const { params } = match({ path: '/not-found' }, routes[0]) + expect(params).toEqual({ pathMatch: '/not-found' }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/custom-query.spec.js b/2.Vue-router/test/unit/specs/custom-query.spec.js new file mode 100644 index 0000000..9ba5988 --- /dev/null +++ b/2.Vue-router/test/unit/specs/custom-query.spec.js @@ -0,0 +1,18 @@ +import Vue from 'vue' +import VueRouter from '../../../src/index' + +Vue.use(VueRouter) + +describe('custom query parse/stringify', () => { + it('should work', () => { + const router = new VueRouter({ + parseQuery: () => ({ foo: 1 }), + stringifyQuery: () => '?foo=1' + }) + + router.push('/?bar=2') + + expect(router.currentRoute.query).toEqual({ foo: 1 }) + expect(router.currentRoute.fullPath).toEqual('/?foo=1') + }) +}) diff --git a/2.Vue-router/test/unit/specs/discrete-components.spec.js b/2.Vue-router/test/unit/specs/discrete-components.spec.js new file mode 100644 index 0000000..a0d1bf0 --- /dev/null +++ b/2.Vue-router/test/unit/specs/discrete-components.spec.js @@ -0,0 +1,22 @@ +import Vue from 'vue' +import VueRouter from '../../../src/index' + +describe('[Vue Instance].$route bindings', () => { + describe('boundToSingleVueInstance', () => { + it('updates $route on all instances', () => { + const router = new VueRouter({ + routes: [ + { path: '/', component: { name: 'foo' }}, + { path: '/bar', component: { name: 'bar' }} + ] + }) + const app1 = new Vue({ router }) + const app2 = new Vue({ router }) + expect(app1.$route.path).toBe('/') + expect(app2.$route.path).toBe('/') + router.push('/bar') + expect(app1.$route.path).toBe('/bar') + expect(app2.$route.path).toBe('/bar') + }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/error-handling.spec.js b/2.Vue-router/test/unit/specs/error-handling.spec.js new file mode 100644 index 0000000..b912dca --- /dev/null +++ b/2.Vue-router/test/unit/specs/error-handling.spec.js @@ -0,0 +1,55 @@ +import Vue from 'vue' +import VueRouter from '../../../src/index' + +Vue.use(VueRouter) + +describe('error handling', () => { + it('onReady errors', () => { + const router = new VueRouter() + const err = new Error('foo') + router.beforeEach(() => { throw err }) + router.onError(() => {}) + + const onReady = jasmine.createSpy('ready') + const onError = jasmine.createSpy('error') + router.onReady(onReady, onError) + + router.push('/') + + expect(onReady).not.toHaveBeenCalled() + expect(onError).toHaveBeenCalledWith(err) + }) + + it('navigation errors', () => { + const router = new VueRouter() + const err = new Error('foo') + const spy = jasmine.createSpy('error') + router.onError(spy) + + router.push('/') + router.beforeEach(() => { throw err }) + + router.push('/foo') + expect(spy).toHaveBeenCalledWith(err) + }) + + it('async component errors', () => { + const err = new Error('foo') + const spy1 = jasmine.createSpy('error') + const spy2 = jasmine.createSpy('errpr') + const Comp = () => { throw err } + const router = new VueRouter({ + routes: [ + { path: '/', component: Comp } + ] + }) + + router.onError(spy1) + router.onReady(() => {}, spy2) + + router.push('/') + + expect(spy1).toHaveBeenCalledWith(err) + expect(spy2).toHaveBeenCalledWith(err) + }) +}) diff --git a/2.Vue-router/test/unit/specs/location.spec.js b/2.Vue-router/test/unit/specs/location.spec.js new file mode 100644 index 0000000..e333421 --- /dev/null +++ b/2.Vue-router/test/unit/specs/location.spec.js @@ -0,0 +1,133 @@ +import { normalizeLocation } from '../../../src/util/location' + +describe('Location utils', () => { + describe('normalizeLocation', () => { + it('string', () => { + const loc = normalizeLocation('/abc?foo=bar&baz=qux#hello') + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/abc') + expect(loc.hash).toBe('#hello') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({ + foo: 'bar', + baz: 'qux' + })) + }) + + it('empty string', function () { + const loc = normalizeLocation('', { path: '/abc' }) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/abc') + expect(loc.hash).toBe('') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({})) + }) + + it('undefined', function () { + const loc = normalizeLocation({}, { path: '/abc' }) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/abc') + expect(loc.hash).toBe('') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({})) + }) + + it('relative', () => { + const loc = normalizeLocation('abc?foo=bar&baz=qux#hello', { + path: '/root/next' + }) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/root/abc') + expect(loc.hash).toBe('#hello') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({ + foo: 'bar', + baz: 'qux' + })) + }) + + it('relative append', () => { + const loc = normalizeLocation('abc?foo=bar&baz=qux#hello', { + path: '/root/next' + }, true) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/root/next/abc') + expect(loc.hash).toBe('#hello') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({ + foo: 'bar', + baz: 'qux' + })) + }) + + it('relative query & hash', () => { + const loc = normalizeLocation('?foo=bar&baz=qux#hello', { + path: '/root/next' + }) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/root/next') + expect(loc.hash).toBe('#hello') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({ + foo: 'bar', + baz: 'qux' + })) + }) + + it('relative params (named)', () => { + const loc = normalizeLocation({ params: { lang: 'fr' }}, { + name: 'hello', + params: { lang: 'en', id: 'foo' } + }) + expect(loc._normalized).toBe(true) + expect(loc.name).toBe('hello') + expect(loc.params).toEqual({ lang: 'fr', id: 'foo' }) + }) + + it('relative params (non-named)', () => { + const loc = normalizeLocation({ params: { lang: 'fr' }}, { + path: '/en/foo', + params: { lang: 'en', id: 'foo' }, + matched: [{ path: '/:lang/:id' }] + }) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/fr/foo') + }) + + it('relative append', () => { + const loc = normalizeLocation({ path: 'a' }, { path: '/b' }, true) + expect(loc.path).toBe('/b/a') + const loc2 = normalizeLocation({ path: 'a', append: true }, { path: '/b' }) + expect(loc2.path).toBe('/b/a') + }) + + it('object', () => { + const loc = normalizeLocation({ + path: '/abc?foo=bar#hello', + query: { baz: 'qux' }, + hash: 'lol' + }) + expect(loc._normalized).toBe(true) + expect(loc.path).toBe('/abc') + expect(loc.hash).toBe('#lol') + expect(JSON.stringify(loc.query)).toBe(JSON.stringify({ + foo: 'bar', + baz: 'qux' + })) + }) + + it('skip normalized', () => { + const loc1 = { + _normalized: true, + path: '/abc?foo=bar#hello', + query: { baz: 'qux' }, + hash: 'lol' + } + const loc2 = normalizeLocation(loc1) + expect(loc1).toBe(loc2) + }) + + it('creates copies when not normalized', () => { + const l1 = { name: 'foo' } + expect(normalizeLocation(l1)).not.toBe(l1) + const l2 = { path: '/foo' } + expect(normalizeLocation(l2)).not.toBe(l2) + const l3 = { path: '/foo', query: { foo: 'foo' }} + expect(normalizeLocation(l3)).not.toBe(l3) + }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/node.spec.js b/2.Vue-router/test/unit/specs/node.spec.js new file mode 100644 index 0000000..d155982 --- /dev/null +++ b/2.Vue-router/test/unit/specs/node.spec.js @@ -0,0 +1,38 @@ +import Vue from 'vue' +import VueRouter from '../../../src/index' + +Vue.use(VueRouter) + +describe('Usage in Node', () => { + it('should be in abstract mode', () => { + const router = new VueRouter() + expect(router.mode).toBe('abstract') + }) + + it('should be able to navigate without app instance', () => { + const router = new VueRouter({ + routes: [ + { path: '/', component: { name: 'foo' }}, + { path: '/bar', component: { name: 'bar' }} + ] + }) + router.push('/bar') + expect(router.history.current.path).toBe('/bar') + }) + + it('getMatchedComponents', () => { + const Foo = { name: 'foo' } + const Bar = { name: 'bar' } + const Baz = { name: 'baz' } + const router = new VueRouter({ + routes: [ + { path: '/', component: Foo }, + { path: '/bar', component: Bar, children: [ + { path: 'baz', component: Baz } + ] } + ] + }) + expect(router.getMatchedComponents('/')).toEqual([Foo]) + expect(router.getMatchedComponents('/bar/baz')).toEqual([Bar, Baz]) + }) +}) diff --git a/2.Vue-router/test/unit/specs/path.spec.js b/2.Vue-router/test/unit/specs/path.spec.js new file mode 100644 index 0000000..bd4e346 --- /dev/null +++ b/2.Vue-router/test/unit/specs/path.spec.js @@ -0,0 +1,77 @@ +import { resolvePath, parsePath, cleanPath } from '../../../src/util/path' + +describe('Path utils', () => { + describe('resolvePath', () => { + it('absolute', () => { + const path = resolvePath('/a', '/b') + expect(path).toBe('/a') + }) + + it('relative', () => { + const path = resolvePath('c/d', '/b') + expect(path).toBe('/c/d') + }) + + it('relative with append', () => { + const path = resolvePath('c/d', '/b', true) + expect(path).toBe('/b/c/d') + }) + + it('relative parent', () => { + const path = resolvePath('../d', '/a/b/c') + expect(path).toBe('/a/d') + }) + + it('relative parent with append', () => { + const path = resolvePath('../d', '/a/b/c', true) + expect(path).toBe('/a/b/d') + }) + + it('relative query', () => { + const path = resolvePath('?foo=bar', '/a/b') + expect(path).toBe('/a/b?foo=bar') + }) + + it('relative hash', () => { + const path = resolvePath('#hi', '/a/b') + expect(path).toBe('/a/b#hi') + }) + }) + + describe('parsePath', () => { + it('plain', () => { + const res = parsePath('/a') + expect(res.path).toBe('/a') + expect(res.hash).toBe('') + expect(res.query).toBe('') + }) + + it('query', () => { + const res = parsePath('/a?foo=bar???') + expect(res.path).toBe('/a') + expect(res.hash).toBe('') + expect(res.query).toBe('foo=bar???') + }) + + it('hash', () => { + const res = parsePath('/a#haha#hoho') + expect(res.path).toBe('/a') + expect(res.hash).toBe('#haha#hoho') + expect(res.query).toBe('') + }) + + it('both', () => { + const res = parsePath('/a?foo=bar#ok?baz=qux') + expect(res.path).toBe('/a') + expect(res.hash).toBe('#ok?baz=qux') + expect(res.query).toBe('foo=bar') + }) + }) + + describe('cleanPath', () => { + it('should work', () => { + const path = cleanPath('//a//b//d/') + expect(path).toBe('/a/b/d/') + }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/query.spec.js b/2.Vue-router/test/unit/specs/query.spec.js new file mode 100644 index 0000000..22c1e6c --- /dev/null +++ b/2.Vue-router/test/unit/specs/query.spec.js @@ -0,0 +1,61 @@ +import { resolveQuery, stringifyQuery } from '../../../src/util/query' + +describe('Query utils', () => { + describe('resolveQuery', () => { + it('should work', () => { + const query = resolveQuery('foo=bar&foo=k', { baz: 'qux' }) + expect(JSON.stringify(query)).toBe( + JSON.stringify({ + foo: ['bar', 'k'], + baz: 'qux' + }) + ) + }) + + it('should turn empty params into null', () => { + expect(resolveQuery('?foo&bar=&arr=1&arr&arr=2')).toEqual({ + foo: null, + bar: '', + arr: ['1', null, '2'] + }) + }) + }) + + describe('stringifyQuery', () => { + it('should work', () => { + expect( + stringifyQuery({ + foo: 'bar', + baz: 'qux', + arr: [1, 2] + }) + ).toBe('?foo=bar&baz=qux&arr=1&arr=2') + }) + + it('should add only the key with null', () => { + expect( + stringifyQuery({ + foo: null, + bar: '', + arr: [1, null, 3] + }) + ).toBe('?foo&bar=&arr=1&arr&arr=3') + }) + + it('should escape reserved chars', () => { + expect( + stringifyQuery({ + a: '*()!' + }) + ).toBe('?a=%2a%28%29%21') + }) + + it('should preserve commas', () => { + expect( + stringifyQuery({ + list: '1,2,3' + }) + ).toBe('?list=1,2,3') + }) + }) +}) diff --git a/2.Vue-router/test/unit/specs/route.spec.js b/2.Vue-router/test/unit/specs/route.spec.js new file mode 100644 index 0000000..a78ed07 --- /dev/null +++ b/2.Vue-router/test/unit/specs/route.spec.js @@ -0,0 +1,128 @@ +import { isSameRoute, isIncludedRoute } from '../../../src/util/route' + +describe('Route utils', () => { + describe('isSameRoute', () => { + it('path', () => { + const a = { + path: '/a', + hash: '#hi', + query: { foo: 'bar', arr: [1, 2] } + } + const b = { + path: '/a/', // Allow trailing slash + hash: '#hi', + query: { arr: ['1', '2'], foo: 'bar' } + } + expect(isSameRoute(a, b)).toBe(true) + }) + + it('name', () => { + const a = { + path: '/abc', + name: 'a', + hash: '#hi', + query: { foo: 'bar', arr: [1, 2] } + } + const b = { + name: 'a', + hash: '#hi', + query: { arr: ['1', '2'], foo: 'bar' } + } + expect(isSameRoute(a, b)).toBe(true) + }) + + it('nested query', () => { + const a = { + path: '/abc', + query: { foo: { bar: 'bar' }, arr: [1, 2] } + } + const b = { + path: '/abc', + query: { arr: [1, 2], foo: { bar: 'bar' }} + } + const c = { + path: '/abc', + query: { arr: [1, 2], foo: { bar: 'not bar' }} + } + expect(isSameRoute(a, b)).toBe(true) + expect(isSameRoute(a, c)).toBe(false) + }) + + it('queries with null values', () => { + const a = { + path: '/abc', + query: { foo: null } + } + const b = { + path: '/abc', + query: { foo: null } + } + const c = { + path: '/abc', + query: { foo: 5 } + } + expect(() => isSameRoute(a, b)).not.toThrow() + expect(() => isSameRoute(a, c)).not.toThrow() + expect(isSameRoute(a, b)).toBe(true) + expect(isSameRoute(a, c)).toBe(false) + }) + }) + + describe('isIncludedRoute', () => { + it('path', () => { + const a = { path: '/a/b' } + const b = { path: '/a' } + const c = { path: '/a/b/c' } + const d = { path: '/a/b/' } + expect(isIncludedRoute(a, b)).toBe(true) + expect(isIncludedRoute(a, c)).toBe(false) + expect(isIncludedRoute(a, d)).toBe(true) + }) + + it('with hash', () => { + const a = { path: '/a/b', hash: '#a' } + const b = { path: '/a' } + const c = { path: '/a', hash: '#a' } + const d = { path: '/a', hash: '#b' } + expect(isIncludedRoute(a, b)).toBe(true) + expect(isIncludedRoute(a, c)).toBe(true) + expect(isIncludedRoute(a, d)).toBe(false) + }) + + it('with query', () => { + const a = { path: '/a/b', query: { foo: 'bar', baz: 'qux' }} + const b = { path: '/a', query: {}} + const c = { path: '/a', query: { foo: 'bar' }} + const d = { path: '/a', query: { foo: 'bar', a: 'b' }} + expect(isIncludedRoute(a, b)).toBe(true) + expect(isIncludedRoute(a, c)).toBe(true) + expect(isIncludedRoute(a, d)).toBe(false) + }) + + it('with both', () => { + const a = { path: '/a/b', query: { foo: 'bar', baz: 'qux' }, hash: '#a' } + const b = { path: '/a', query: {}} + const c = { path: '/a', query: { foo: 'bar' }} + const d = { path: '/a', query: { foo: 'bar' }, hash: '#b' } + const e = { path: '/a', query: { a: 'b' }, hash: '#a' } + expect(isIncludedRoute(a, b)).toBe(true) + expect(isIncludedRoute(a, c)).toBe(true) + expect(isIncludedRoute(a, d)).toBe(false) + expect(isIncludedRoute(a, e)).toBe(false) + }) + + it('trailing slash', () => { + const a = { path: '/users' } + const b = { path: '/user' } + const c = { path: '/users/' } + expect(isIncludedRoute(a, b)).toBe(false) + expect(isIncludedRoute(a, c)).toBe(true) + + const d = { path: '/users/hello/world' } + const e = { path: '/users/hello' } + const f = { path: '/users/hello-world' } + expect(isIncludedRoute(d, e)).toBe(true) + expect(isIncludedRoute(d, f)).toBe(false) + }) + }) +})