Remove page.js in favor of our own re-written version of it
Release-Notes: skip
Change-Id: I61aed543a197b0b004af24c48c2ccf673c9ab839
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
new file mode 100644
index 0000000..3a49c32
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page.ts
@@ -0,0 +1,372 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * This file was originally a copy of https://github.com/visionmedia/page.js.
+ * It was converted to TypeScript and stripped off lots of code that we don't
+ * need in Gerrit. Thus we reproduce the original LICENSE in js_licenses.txt.
+ */
+
+/**
+ * This is what registered routes have to provide, see `registerRoute()` and
+ * `registerExitRoute()`.
+ * `context` provides information about the matched parameters in the URL.
+ * Then you can decide to handle the route exclusively (not calling `next()`),
+ * or to pass it on to other registered routes. Normally you would not call
+ * `next()`, because your regex matching the URL was specific enough.
+ */
+export type PageCallback = (
+ context: PageContext,
+ next: PageNextCallback
+) => void;
+
+/** See comment on `PageCallback` above. */
+export type PageNextCallback = () => void;
+
+/** Options for starting the router. */
+export interface PageOptions {
+ /**
+ * Should a `popstate` listener be installed? The default is `true`. Can be
+ * turned off for testing.
+ */
+ popstate: boolean;
+ /**
+ * Should the router inspect the current URL and dispatch it when the router
+ * is started? Default is `true`, but can be turned off for testing.
+ */
+ dispatch: boolean;
+
+ /**
+ * The base path of the application. For Gerrit this must be set to
+ * getBaseUrl().
+ */
+ base: string;
+}
+
+/**
+ * The browser `History` API allows `pushState()` to contain an arbitrary state
+ * object. Our router only sets `path` on the state and inspects it when
+ * handling `popstate` events. This interface is internal only.
+ */
+interface PageState {
+ path?: string;
+}
+
+const clickEvent = document.ontouchstart ? 'touchstart' : 'click';
+
+export class Page {
+ /**
+ * When a new URL is dispatched all these routes are called one after another.
+ * If a route decides that it wants to handle a URL, then it does not call
+ * next().
+ */
+ private entryRoutes: PageCallback[] = [];
+
+ /**
+ * Before a new URL is dispatched exit routes for the previous URL are called.
+ * They can clean up some state for example. But they could also prevent the
+ * user from navigating away (from within the app), if they don't call next().
+ */
+ private exitRoutes: PageCallback[] = [];
+
+ /**
+ * The path that is currently being dispatched. This is used, so that we can
+ * check whether a context is still valid, i.e. ctx.path === currentPath.
+ */
+ private currentPath = '';
+
+ /**
+ * The base path of the application. For Gerrit this must be set to
+ * getBaseUrl(). For example https://gerrit.wikimedia.org/ uses r/ as its
+ * base path.
+ */
+ private base = '';
+
+ /**
+ * Is set at the beginning of start() and stop(), so that you cannot start
+ * the routing twice.
+ */
+ private running = false;
+
+ /**
+ * Keeping around the previous context for being able to call exit routes
+ * after creating a new context.
+ */
+ private prevPageContext?: PageContext;
+
+ /**
+ * We don't want to handle popstate events before the document is loaded.
+ */
+ private documentLoaded = false;
+
+ start(options: PageOptions = {dispatch: true, popstate: true, base: ''}) {
+ if (this.running) return;
+ this.running = true;
+ this.base = options.base;
+
+ window.document.addEventListener(clickEvent, this.clickHandler);
+ window.addEventListener('load', this.loadHandler);
+ if (options.popstate) {
+ window.addEventListener('popstate', this.popStateHandler);
+ }
+ if (document.readyState === 'complete') this.documentLoaded = true;
+
+ if (options.dispatch) {
+ const loc = window.location;
+ this.replace(loc.pathname + loc.search + loc.hash);
+ }
+ }
+
+ stop() {
+ if (!this.running) return;
+ this.currentPath = '';
+ this.running = false;
+
+ window.document.removeEventListener(clickEvent, this.clickHandler);
+ window.removeEventListener('popstate', this.popStateHandler);
+ window.removeEventListener('load', this.loadHandler);
+ }
+
+ show(path: string, push = true) {
+ const ctx = new PageContext(path, {}, this.base);
+ const prev = this.prevPageContext;
+ this.prevPageContext = ctx;
+ this.currentPath = ctx.path;
+ this.dispatch(ctx, prev);
+ if (push && !ctx.preventPush) ctx.pushState();
+ }
+
+ redirect(to: string) {
+ setTimeout(() => this.replace(to), 0);
+ }
+
+ replace(path: string, state: PageState = {}, dispatch = true) {
+ const ctx = new PageContext(path, state, this.base);
+ const prev = this.prevPageContext;
+ this.prevPageContext = ctx;
+ this.currentPath = ctx.path;
+ ctx.replaceState(); // replace before dispatching, which may redirect
+ if (dispatch) this.dispatch(ctx, prev);
+ }
+
+ dispatch(ctx: PageContext, prev?: PageContext) {
+ let j = 0;
+ const nextExit = () => {
+ const fn = this.exitRoutes[j++];
+ // First call the exit routes of the previous context. Then proceed
+ // to the entry routes for the new context.
+ if (!fn) {
+ nextEnter();
+ return;
+ }
+ fn(prev!, nextExit);
+ };
+
+ let i = 0;
+ const nextEnter = () => {
+ const fn = this.entryRoutes[i++];
+
+ // Concurrency protection. The context is not valid anymore.
+ // Stop calling any further route handlers.
+ if (ctx.path !== this.currentPath) {
+ ctx.preventPush = true;
+ return;
+ }
+
+ // You must register a route that handles everything (.*) and does not
+ // call next().
+ if (!fn) throw new Error('No route has handled the URL.');
+
+ fn(ctx, nextEnter);
+ };
+
+ if (prev) {
+ nextExit();
+ } else {
+ nextEnter();
+ }
+ }
+
+ registerRoute(re: RegExp, fn: PageCallback) {
+ this.entryRoutes.push(createRoute(re, fn));
+ }
+
+ registerExitRoute(re: RegExp, fn: PageCallback) {
+ this.exitRoutes.push(createRoute(re, fn));
+ }
+
+ loadHandler = () => {
+ setTimeout(() => (this.documentLoaded = true), 0);
+ };
+
+ clickHandler = (e: MouseEvent | TouchEvent) => {
+ if ((e as MouseEvent).button !== 0) return;
+ if (e.metaKey || e.ctrlKey || e.shiftKey) return;
+ if (e.defaultPrevented) return;
+
+ let el = e.target as HTMLAnchorElement;
+ const eventPath = e.composedPath();
+ if (eventPath) {
+ for (let i = 0; i < eventPath.length; i++) {
+ const pathEl = eventPath[i] as HTMLAnchorElement;
+ if (!pathEl.nodeName) continue;
+ if (pathEl.nodeName.toUpperCase() !== 'A') continue;
+ if (!pathEl.href) continue;
+
+ el = pathEl;
+ break;
+ }
+ }
+
+ while (el && 'A' !== el.nodeName.toUpperCase())
+ el = el.parentNode as HTMLAnchorElement;
+ if (!el || 'A' !== el.nodeName.toUpperCase()) return;
+
+ if (el.hasAttribute('download') || el.getAttribute('rel') === 'external')
+ return;
+ const link = el.getAttribute('href');
+ if (samePath(el) && (el.hash || '#' === link)) return;
+ if (link && link.indexOf('mailto:') > -1) return;
+ if (el.target) return;
+ if (!sameOrigin(el.href)) return;
+
+ let path = el.pathname + el.search + (el.hash ?? '');
+ path = path[0] !== '/' ? '/' + path : path;
+
+ const orig = path;
+ if (path.indexOf(this.base) === 0) {
+ path = path.substr(this.base.length);
+ }
+ if (this.base && orig === path && window.location.protocol !== 'file:') {
+ return;
+ }
+ e.preventDefault();
+ this.show(orig);
+ };
+
+ popStateHandler = () => (e: PopStateEvent) => {
+ if (!this.documentLoaded) return;
+ if (e.state) {
+ const path = e.state.path;
+ this.replace(path, e.state);
+ } else {
+ const loc = window.location;
+ this.show(loc.pathname + loc.search + loc.hash, /* push */ false);
+ }
+ };
+}
+
+function sameOrigin(href: string) {
+ if (!href) return false;
+ const url = new URL(href, window.location.toString());
+ const loc = window.location;
+ return (
+ loc.protocol === url.protocol &&
+ loc.hostname === url.hostname &&
+ loc.port === url.port
+ );
+}
+
+function samePath(url: HTMLAnchorElement) {
+ const loc = window.location;
+ return url.pathname === loc.pathname && url.search === loc.search;
+}
+
+function escapeRegExp(s: string) {
+ return s.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
+}
+
+function decodeURIComponentString(val: string | undefined | null) {
+ if (!val) return '';
+ return decodeURIComponent(val.replace(/\+/g, ' '));
+}
+
+export class PageContext {
+ /**
+ * Includes everything: base, path, query and hash.
+ */
+ canonicalPath = '';
+
+ /**
+ * Does not include base path.
+ * Does not include hash.
+ * Includes query string.
+ */
+ path = '';
+
+ /** Does not include hash. */
+ querystring = '';
+
+ hash = '';
+
+ /**
+ * Regular expression matches of capturing groups. The first entry params[0]
+ * corresponds to the first capturing group. The entire matched string is not
+ * returned in this array.
+ */
+ params: string[] = [];
+
+ /**
+ * Prevents `show()` from eventually calling `pushState()`. For example if
+ * the current context is not "valid" anymore, i.e. the URL has changed in the
+ * meantime.
+ *
+ * This is router internal state. Do not use it from routes.
+ */
+ preventPush = false;
+
+ private title = '';
+
+ constructor(
+ path: string,
+ private readonly state: PageState = {},
+ pageBase = ''
+ ) {
+ this.title = window.document.title;
+
+ if ('/' === path[0] && 0 !== path.indexOf(pageBase)) path = pageBase + path;
+ this.canonicalPath = path;
+ const re = new RegExp('^' + escapeRegExp(pageBase));
+ this.path = path.replace(re, '') || '/';
+ this.state.path = path;
+
+ const i = path.indexOf('?');
+ this.querystring =
+ i !== -1 ? decodeURIComponentString(path.slice(i + 1)) : '';
+
+ // Does the path include a hash? If yes, then remove it from path and
+ // querystring.
+ if (this.path.indexOf('#') === -1) return;
+ const parts = this.path.split('#');
+ this.path = parts[0];
+ this.hash = decodeURIComponentString(parts[1]) || '';
+ this.querystring = this.querystring.split('#')[0];
+ }
+
+ pushState() {
+ window.history.pushState(this.state, this.title, this.canonicalPath);
+ }
+
+ replaceState() {
+ window.history.replaceState(this.state, this.title, this.canonicalPath);
+ }
+}
+
+function createRoute(re: RegExp, fn: Function) {
+ return (ctx: PageContext, next: Function) => {
+ const qsIndex = ctx.path.indexOf('?');
+ const pathname = qsIndex !== -1 ? ctx.path.slice(0, qsIndex) : ctx.path;
+ const matches = re.exec(decodeURIComponent(pathname));
+ if (matches) {
+ ctx.params = matches
+ .slice(1)
+ .map(match => decodeURIComponentString(match));
+ fn(ctx, next);
+ } else {
+ next();
+ }
+ };
+}
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
new file mode 100644
index 0000000..6691bd8
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-page_test.ts
@@ -0,0 +1,98 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import {html, assert, fixture, waitUntil} from '@open-wc/testing';
+import './gr-router';
+import {Page, PageContext} from './gr-page';
+
+suite('gr-page tests', () => {
+ let page: Page;
+
+ setup(() => {
+ page = new Page();
+ page.start({dispatch: false, popstate: false, base: ''});
+ });
+
+ teardown(() => {
+ page.stop();
+ });
+
+ test('click handler', async () => {
+ const spy = sinon.spy();
+ page.registerRoute(/\/settings/, spy);
+ const link = await fixture<HTMLAnchorElement>(
+ html`<a href="/settings"></a>`
+ );
+ link.click();
+ assert.isTrue(spy.calledOnce);
+ });
+
+ test('register route and exit', () => {
+ const handleA = sinon.spy();
+ const handleAExit = sinon.stub();
+ page.registerRoute(/\/A/, handleA);
+ page.registerExitRoute(/\/A/, handleAExit);
+
+ page.show('/A');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleAExit.callCount, 0);
+
+ page.show('/B');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleAExit.callCount, 1);
+ });
+
+ test('register, show, replace', () => {
+ const handleA = sinon.spy();
+ const handleB = sinon.stub();
+ page.registerRoute(/\/A/, handleA);
+ page.registerRoute(/\/B/, handleB);
+
+ page.show('/A');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleB.callCount, 0);
+
+ page.show('/B');
+ assert.equal(handleA.callCount, 1);
+ assert.equal(handleB.callCount, 1);
+
+ page.replace('/A');
+ assert.equal(handleA.callCount, 2);
+ assert.equal(handleB.callCount, 1);
+
+ page.replace('/B');
+ assert.equal(handleA.callCount, 2);
+ assert.equal(handleB.callCount, 2);
+ });
+
+ test('register pattern, check context', async () => {
+ let context: PageContext;
+ const handler = (ctx: PageContext) => (context = ctx);
+ page.registerRoute(/\/asdf\/(.*)\/qwer\/(.*)\//, handler);
+ page.stop();
+ page.start({dispatch: false, popstate: false, base: '/base'});
+
+ page.show('/base/asdf/1234/qwer/abcd/');
+
+ await waitUntil(() => !!context);
+ assert.equal(context!.canonicalPath, '/base/asdf/1234/qwer/abcd/');
+ assert.equal(context!.path, '/asdf/1234/qwer/abcd/');
+ assert.equal(context!.querystring, '');
+ assert.equal(context!.hash, '');
+ assert.equal(context!.params[0], '1234');
+ assert.equal(context!.params[1], 'abcd');
+
+ page.show('/asdf//qwer////?a=b#go');
+
+ await waitUntil(() => !!context);
+ assert.equal(context!.canonicalPath, '/base/asdf//qwer////?a=b#go');
+ assert.equal(context!.path, '/asdf//qwer////?a=b');
+ assert.equal(context!.querystring, 'a=b');
+ assert.equal(context!.hash, 'go');
+ assert.equal(context!.params[0], '');
+ assert.equal(context!.params[1], '//');
+ });
+});
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
index 264560b..d0b545f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.ts
@@ -3,12 +3,7 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {
- Options,
- page,
- PageContext,
- PageNextCallback,
-} from '../../../utils/page-wrapper-utils';
+import {Page, PageOptions, PageContext, PageNextCallback} from './gr-page';
import {NavigationService} from '../gr-navigation/gr-navigation';
import {getAppContext} from '../../../services/app-context';
import {
@@ -108,7 +103,7 @@
// TODO: Move all patterns to view model files and use the `Route` interface,
// which will enforce using `RegExp` in its `urlPattern` property.
const RoutePattern = {
- ROOT: '/',
+ ROOT: /^\/$/,
DASHBOARD: /^\/dashboard\/(.+)$/,
CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
@@ -302,7 +297,7 @@
private view?: GerritView;
- readonly page = page.create();
+ readonly page = new Page();
constructor(
private readonly reporting: ReportingService,
@@ -340,12 +335,7 @@
}
if (browserUrl.toString() !== stateUrl.toString()) {
- this.page.replace(
- stateUrl.toString(),
- null,
- /* init: */ false,
- /* dispatch: */ false
- );
+ this.page.replace(stateUrl.toString(), {}, /* dispatch: */ false);
}
}),
this.routerModel.routerView$.subscribe(view => (this.view = view)),
@@ -429,13 +419,13 @@
*/
redirectToLogin(returnUrl: string) {
const basePath = getBaseUrl() || '';
- this.page(
+ this.setUrl(
'/login/' + encodeURIComponent(returnUrl.substring(basePath.length))
);
}
/**
- * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
+ * Hashes parsed by gr-page exclude "inner" hashes, so a URL like "/a#b#c"
* is parsed to have a hash of "b" rather than "b#c". Instead, this method
* parses hashes correctly. Will return an empty string if there is no hash.
*
@@ -464,18 +454,18 @@
* @return A promise yielding the original route ctx
* (if it resolves).
*/
- redirectIfNotLoggedIn(ctx: PageContext) {
+ redirectIfNotLoggedIn(path: string) {
return this.restApiService.getLoggedIn().then(loggedIn => {
if (loggedIn) {
return Promise.resolve();
} else {
- this.redirectToLogin(ctx.canonicalPath);
+ this.redirectToLogin(path);
return Promise.reject(new Error());
}
});
}
- /** Page.js middleware that warms the REST API's logged-in cache line. */
+ /** gr-page middleware that warms the REST API's logged-in cache line. */
private loadUserMiddleware(_: PageContext, next: PageNextCallback) {
this.restApiService.getLoggedIn().then(() => {
next();
@@ -485,11 +475,10 @@
/**
* Map a route to a method on the router.
*
- * @param pattern The page.js pattern for the route.
+ * @param pattern The regex pattern for the route.
* @param handlerName The method name for the handler. If the
* route is matched, the handler will be executed with `this` referring
- * to the component. Its return value will be discarded so that it does
- * not interfere with page.js.
+ * to the component. Its return value will be discarded.
* TODO: Get rid of this parameter. This is really not something that the
* router wants to be concerned with. The reporting service and the view
* models should figure that out between themselves.
@@ -499,24 +488,23 @@
* redirect specifies the matched URL to be used after successful auth.
*/
mapRoute(
- pattern: string | RegExp,
+ pattern: RegExp,
handlerName: string,
handler: (ctx: PageContext) => void,
authRedirect?: boolean
) {
- this.page(
- pattern,
- (ctx, next) => this.loadUserMiddleware(ctx, next),
- ctx => {
- this.reporting.locationChanged(handlerName);
- const promise = authRedirect
- ? this.redirectIfNotLoggedIn(ctx)
- : Promise.resolve();
- promise.then(() => {
- handler(ctx);
- });
- }
+ this.page.registerRoute(pattern, (ctx, next) =>
+ this.loadUserMiddleware(ctx, next)
);
+ this.page.registerRoute(pattern, ctx => {
+ this.reporting.locationChanged(handlerName);
+ const promise = authRedirect
+ ? this.redirectIfNotLoggedIn(ctx.canonicalPath)
+ : Promise.resolve();
+ promise.then(() => {
+ handler(ctx);
+ });
+ });
}
/**
@@ -583,16 +571,13 @@
}
_testOnly_startRouter() {
- this.startRouter({dispatch: false, popstate: false});
+ this.startRouter({dispatch: false, popstate: false, base: getBaseUrl()});
}
- startRouter(opts: Options = {}) {
- const base = getBaseUrl();
- if (base) {
- this.page.base(base);
- }
-
- this.page.exit('*', (_, next) => {
+ startRouter(
+ opts: PageOptions = {dispatch: true, popstate: true, base: getBaseUrl()}
+ ) {
+ this.page.registerExitRoute(/(.*)/, (_, next) => {
if (!this._isRedirecting) {
this.reporting.beforeLocationChanged();
}
@@ -603,7 +588,7 @@
// Remove the tracking param 'usp' (User Source Parameter) from the URL,
// just to have users look at cleaner URLs.
- this.page((ctx, next) => {
+ this.page.registerRoute(/(.*)/, (ctx, next) => {
if (window.URLSearchParams) {
const pathname = toPathname(ctx.canonicalPath);
const searchParams = toSearchParams(ctx.canonicalPath);
@@ -619,7 +604,7 @@
});
// Middleware
- this.page((ctx, next) => {
+ this.page.registerRoute(/(.*)/, (ctx, next) => {
document.body.scrollTop = 0;
if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
@@ -937,7 +922,7 @@
// For backward compatibility with GWT links.
if (hash) {
// In certain login flows the server may redirect to a hash without
- // a leading slash, which page.js doesn't handle correctly.
+ // a leading slash, which gr-page doesn't handle correctly.
if (hash[0] !== '/') {
hash = '/' + hash;
}
@@ -1087,7 +1072,7 @@
const state: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.GROUPS,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
openCreateModal:
!ctx.params[1] && !ctx.params[2] && ctx.hash === 'create',
@@ -1184,7 +1169,7 @@
view: GerritView.REPO,
detail: RepoDetailView.BRANCHES,
repo: ctx.params[0] as RepoName,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
};
// Note that router model view must be updated before view models.
@@ -1197,7 +1182,7 @@
view: GerritView.REPO,
detail: RepoDetailView.TAGS,
repo: ctx.params[0] as RepoName,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
};
// Note that router model view must be updated before view models.
@@ -1209,7 +1194,7 @@
const state: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.REPOS,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
openCreateModal:
!ctx.params[1] && !ctx.params[2] && ctx.hash === 'create',
@@ -1239,7 +1224,7 @@
const state: AdminViewState = {
view: GerritView.ADMIN,
adminView: AdminChildView.PLUGINS,
- offset: ctx.params[2] ?? '0',
+ offset: ctx.params[2] || '0',
filter: ctx.params[1] ?? null,
};
// Note that router model view must be updated before view models.
@@ -1251,7 +1236,7 @@
const state: SearchViewState = {
view: GerritView.SEARCH,
query: ctx.params[0],
- offset: ctx.params[2],
+ offset: ctx.params[2] || '0',
loading: false,
changes: [],
};
@@ -1267,7 +1252,7 @@
const state: SearchViewState = {
view: GerritView.SEARCH,
query: ctx.params[0],
- offset: undefined,
+ offset: '0',
loading: false,
changes: [],
};
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
index 3ca7bdc..565ddd9 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.ts
@@ -5,7 +5,7 @@
*/
import '../../../test/common-test-setup';
import './gr-router';
-import {Page, PageContext} from '../../../utils/page-wrapper-utils';
+import {Page, PageContext} from './gr-page';
import {
stubBaseUrl,
stubRestApi,
@@ -125,7 +125,6 @@
const requiresAuth: any = {};
const doesNotRequireAuth: any = {};
sinon.stub(page, 'start');
- sinon.stub(page, 'base');
sinon
.stub(router, 'mapRoute')
.callsFake((_pattern, methodName, _method, usesAuth) => {
@@ -212,20 +211,8 @@
test('redirectIfNotLoggedIn while logged in', () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(true));
- const ctx = {
- save() {},
- handled: true,
- canonicalPath: '',
- path: '',
- querystring: '',
- pathname: '',
- state: '',
- title: '',
- hash: '',
- params: {test: 'test'},
- };
const redirectStub = sinon.stub(router, 'redirectToLogin');
- return router.redirectIfNotLoggedIn(ctx).then(() => {
+ return router.redirectIfNotLoggedIn('somepath').then(() => {
assert.isFalse(redirectStub.called);
});
});
@@ -233,21 +220,9 @@
test('redirectIfNotLoggedIn while logged out', () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
const redirectStub = sinon.stub(router, 'redirectToLogin');
- const ctx = {
- save() {},
- handled: true,
- canonicalPath: '',
- path: '',
- querystring: '',
- pathname: '',
- state: '',
- title: '',
- hash: '',
- params: {test: 'test'},
- };
return new Promise(resolve => {
router
- .redirectIfNotLoggedIn(ctx)
+ .redirectIfNotLoggedIn('somepath')
.then(() => {
assert.isTrue(false, 'Should never execute');
})
@@ -321,17 +296,6 @@
await waitUntilCalled(handlePassThroughRoute, 'handlePassThroughRoute');
}
- function createPageContext(): PageContext {
- return {
- canonicalPath: '',
- path: '',
- querystring: '',
- pathname: '',
- hash: '',
- params: {},
- };
- }
-
setup(() => {
stubRestApi('setInProjectLookup');
redirectStub = sinon.stub(router, 'redirect');
@@ -395,9 +359,8 @@
) => {
onExit = _onExit;
};
- sinon.stub(page, 'exit').callsFake(onRegisteringExit);
+ sinon.stub(page, 'registerExitRoute').callsFake(onRegisteringExit);
sinon.stub(page, 'start');
- sinon.stub(page, 'base');
router._testOnly_startRouter();
router.handleDefaultRoute();
@@ -465,7 +428,10 @@
suite('ROOT', () => {
test('closes for closeAfterLogin', () => {
- const ctx = {...createPageContext(), querystring: 'closeAfterLogin'};
+ const ctx = {
+ querystring: 'closeAfterLogin',
+ canonicalPath: '',
+ } as PageContext;
const closeStub = sinon.stub(window, 'close');
const result = router.handleRootRoute(ctx);
assert.isNotOk(result);
@@ -586,7 +552,7 @@
adminView: AdminChildView.GROUPS,
offset: '0',
openCreateModal: false,
- filter: null,
+ filter: '',
};
await checkUrlToState('/admin/groups', defaultState);
@@ -1020,7 +986,7 @@
view: GerritView.DOCUMENTATION_SEARCH,
filter: 'asdf',
});
- // Percent decoding works fine. page.js decodes twice, so the only problem
+ // Percent decoding works fine. gr-page decodes twice, so the only problem
// is having `%25` in the URL, because the first decoding pass will yield
// `%`, and then the second decoding pass will throw `URI malformed`.
await checkUrlToState('/Documentation/q/filter:as%20%2fdf', {