Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/mr-app/mr-app.test.js b/static_src/elements/mr-app/mr-app.test.js
new file mode 100644
index 0000000..47b953b
--- /dev/null
+++ b/static_src/elements/mr-app/mr-app.test.js
@@ -0,0 +1,300 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {assert} from 'chai';
+import sinon from 'sinon';
+import {MrApp} from './mr-app.js';
+import {store, resetState} from 'reducers/base.js';
+import {select} from 'reducers/projectV0.js';
+
+let element;
+let next;
+
+window.CS_env = {
+  token: 'foo-token',
+};
+
+describe('mr-app', () => {
+  beforeEach(() => {
+    global.ga = sinon.spy();
+    store.dispatch(resetState());
+    element = document.createElement('mr-app');
+    document.body.appendChild(element);
+    element.formsToCheck = [];
+
+    next = sinon.stub();
+  });
+
+  afterEach(() => {
+    global.ga.resetHistory();
+    document.body.removeChild(element);
+    next.reset();
+  });
+
+  it('initializes', () => {
+    assert.instanceOf(element, MrApp);
+  });
+
+  describe('snackbar handling', () => {
+    beforeEach(() => {
+      sinon.spy(store, 'dispatch');
+    });
+
+    afterEach(() => {
+      store.dispatch.restore();
+    });
+
+    it('renders no snackbars', async () => {
+      element._snackbars = [];
+
+      await element.updateComplete;
+
+      const snackbars = element.querySelectorAll('chops-snackbar');
+
+      assert.equal(snackbars.length, 0);
+    });
+
+    it('renders multiple snackbars', async () => {
+      element._snackbars = [
+        {text: 'Snackbar one', id: 'one'},
+        {text: 'Snackbar two', id: 'two'},
+        {text: 'Snackbar three', id: 'thre'},
+      ];
+
+      await element.updateComplete;
+
+      const snackbars = element.querySelectorAll('chops-snackbar');
+
+      assert.equal(snackbars.length, 3);
+
+      assert.include(snackbars[0].textContent, 'Snackbar one');
+      assert.include(snackbars[1].textContent, 'Snackbar two');
+      assert.include(snackbars[2].textContent, 'Snackbar three');
+    });
+
+    it('closing snackbar hides snackbar', async () => {
+      element._snackbars = [
+        {text: 'Snackbar', id: 'one'},
+      ];
+
+      await element.updateComplete;
+
+      const snackbar = element.querySelector('chops-snackbar');
+
+      snackbar.close();
+
+      sinon.assert.calledWith(store.dispatch,
+          {type: 'HIDE_SNACKBAR', id: 'one'});
+    });
+  });
+
+  it('_preRouteHandler calls next()', () => {
+    const ctx = {params: {}};
+
+    element._preRouteHandler(ctx, next);
+
+    sinon.assert.calledOnce(next);
+  });
+
+  it('_preRouteHandler does not call next() on same page nav', () => {
+    element._lastContext = {path: '123'};
+    const ctx = {params: {}, path: '123'};
+
+    element._preRouteHandler(ctx, next);
+
+    assert.isFalse(ctx.handled);
+    sinon.assert.notCalled(next);
+  });
+
+  it('_preRouteHandler parses queryParams', () => {
+    const ctx = {params: {}, querystring: 'q=owner:me&colspec=Summary'};
+    element._preRouteHandler(ctx, next);
+
+    assert.deepEqual(ctx.queryParams, {q: 'owner:me', colspec: 'Summary'});
+  });
+
+  it('_preRouteHandler ignores case for queryParams keys', () => {
+    const ctx = {params: {},
+      querystring: 'Q=owner:me&ColSpeC=Summary&x=owner'};
+    element._preRouteHandler(ctx, next);
+
+    assert.deepEqual(ctx.queryParams, {q: 'owner:me', colspec: 'Summary',
+      x: 'owner'});
+  });
+
+  it('_preRouteHandler ignores case for queryParams keys', () => {
+    const ctx = {params: {},
+      querystring: 'Q=owner:me&ColSpeC=Summary&x=owner'};
+    element._preRouteHandler(ctx, next);
+
+    assert.deepEqual(ctx.queryParams, {q: 'owner:me', colspec: 'Summary',
+      x: 'owner'});
+  });
+
+  it('_postRouteHandler saves ctx.queryParams to Redux', () => {
+    const ctx = {queryParams: {q: '1234'}};
+    element._postRouteHandler(ctx, next);
+
+    assert.deepEqual(element.queryParams, {q: '1234'});
+  });
+
+  it('_postRouteHandler saves ctx to this._lastContext', () => {
+    const ctx = {path: '1234'};
+    element._postRouteHandler(ctx, next);
+
+    assert.deepEqual(element._lastContext, {path: '1234'});
+  });
+
+  describe('scroll to the top on page changes', () => {
+    beforeEach(() => {
+      sinon.stub(window, 'scrollTo');
+    });
+
+    afterEach(() => {
+      window.scrollTo.restore();
+    });
+
+    it('scrolls page to top on initial load', () => {
+      element._lastContext = null;
+      const ctx = {params: {}, path: '1234'};
+      element._postRouteHandler(ctx, next);
+
+      sinon.assert.calledWith(window.scrollTo, 0, 0);
+    });
+
+    it('scrolls page to top on parh change', () => {
+      element._lastContext = {params: {}, pathname: '/list',
+        path: '/list?q=123', querystring: '?q=123', queryParams: {q: '123'}};
+      const ctx = {params: {}, pathname: '/other',
+        path: '/other?q=123', querystring: '?q=123', queryParams: {q: '123'}};
+
+      element._postRouteHandler(ctx, next);
+
+      sinon.assert.calledWith(window.scrollTo, 0, 0);
+    });
+
+    it('does not scroll to top when on the same path', () => {
+      element._lastContext = {pathname: '/list', path: '/list?q=123',
+        querystring: '?a=123', queryParams: {a: '123'}};
+      const ctx = {pathname: '/list', path: '/list?q=456',
+        querystring: '?a=456', queryParams: {a: '456'}};
+
+      element._postRouteHandler(ctx, next);
+
+      sinon.assert.notCalled(window.scrollTo);
+    });
+
+    it('scrolls to the top on same path when q param changes', () => {
+      element._lastContext = {pathname: '/list', path: '/list?q=123',
+        querystring: '?q=123', queryParams: {q: '123'}};
+      const ctx = {pathname: '/list', path: '/list?q=456',
+        querystring: '?q=456', queryParams: {q: '456'}};
+
+      element._postRouteHandler(ctx, next);
+
+      sinon.assert.calledWith(window.scrollTo, 0, 0);
+    });
+  });
+
+
+  it('_postRouteHandler does not call next', () => {
+    const ctx = {path: '1234'};
+    element._postRouteHandler(ctx, next);
+
+    sinon.assert.notCalled(next);
+  });
+
+  it('_loadIssuePage loads issue page', async () => {
+    await element._loadIssuePage({
+      queryParams: {id: '234'},
+      params: {project: 'chromium'},
+    }, next);
+    await element.updateComplete;
+
+    // Check that only one page element is rendering at a time.
+    const main = element.querySelector('main');
+    assert.equal(main.children.length, 1);
+
+    const issuePage = element.querySelector('mr-issue-page');
+    assert.isDefined(issuePage, 'issue page is defined');
+    assert.equal(issuePage.issueRef.projectName, 'chromium');
+    assert.equal(issuePage.issueRef.localId, 234);
+  });
+
+  it('_loadListPage loads list page', async () => {
+    await element._loadListPage({
+      params: {project: 'chromium'},
+    }, next);
+    await element.updateComplete;
+
+    // Check that only one page element is rendering at a time.
+    const main = element.querySelector('main');
+    assert.equal(main.children.length, 1);
+
+    const listPage = element.querySelector('mr-list-page');
+    assert.isDefined(listPage, 'list page is defined');
+  });
+
+  it('_loadListPage loads grid page', async () => {
+    element.queryParams = {mode: 'grid'};
+    await element._loadListPage({
+      params: {project: 'chromium'},
+    }, next);
+    await element.updateComplete;
+
+    // Check that only one page element is rendering at a time.
+    const main = element.querySelector('main');
+    assert.equal(main.children.length, 1);
+
+    const gridPage = element.querySelector('mr-grid-page');
+    assert.isDefined(gridPage, 'grid page is defined');
+  });
+
+  describe('_selectProject', () => {
+    beforeEach(() => {
+      sinon.spy(store, 'dispatch');
+    });
+
+    afterEach(() => {
+      store.dispatch.restore();
+    });
+
+    it('selects and fetches project', () => {
+      const projectName = 'chromium';
+      assert.notEqual(store.getState().projectV0.name, projectName);
+
+      element._selectProject(projectName);
+
+      sinon.assert.calledTwice(store.dispatch);
+    });
+
+    it('skips selecting and fetching when project isn\'t changing', () => {
+      const projectName = 'chromium';
+
+      store.dispatch.restore();
+      store.dispatch(select(projectName));
+      sinon.spy(store, 'dispatch');
+
+      assert.equal(store.getState().projectV0.name, projectName);
+
+      element._selectProject(projectName);
+
+      sinon.assert.notCalled(store.dispatch);
+    });
+
+    it('selects without fetching when transitioning to null', () => {
+      const projectName = 'chromium';
+
+      store.dispatch.restore();
+      store.dispatch(select(projectName));
+      sinon.spy(store, 'dispatch');
+
+      assert.equal(store.getState().projectV0.name, projectName);
+
+      element._selectProject(null);
+
+      sinon.assert.calledOnce(store.dispatch);
+    });
+  });
+});