blob: 913a914d8e8c164fe487c2fb96e31e7a0e346ad1 [file] [log] [blame]
<!DOCTYPE html>
<!--
@license
Copyright (C) 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<meta charset="utf-8">
<title>gr-change-view</title>
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/components/wct-browser-legacy/browser.js"></script>
<script src="/node_modules/page/page.js"></script>
<test-fixture id="basic">
<template>
<gr-change-view></gr-change-view>
</template>
</test-fixture>
<test-fixture id="blank">
<template>
<div></div>
</template>
</test-fixture>
<script type="module">
import '../../../test/common-test-setup.js';
import '../../edit/gr-edit-constants.js';
import './gr-change-view.js';
import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
import {util} from '../../../scripts/util.js';
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
const pluginApi = _testOnly_initGerritPluginApi();
suite('gr-change-view tests', () => {
const kb = KeyboardShortcutBinder;
kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter');
kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r');
kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a');
kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd');
kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm');
kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's');
kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u');
kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x');
kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z');
kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ',');
kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't');
let element;
let sandbox;
let navigateToChangeStub;
const TEST_SCROLL_TOP_PX = 100;
const ROBOT_COMMENTS_LIMIT = 10;
const THREADS = [
{
comments: [
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 2,
robot_id: 'rb1',
id: 'ecf9fa_fe1a5f62',
line: 5,
updated: '2018-02-08 18:49:18.000000000',
message: 'test',
unresolved: true,
},
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 4,
id: 'ecf0b9fa_fe1a5f62',
line: 5,
updated: '2018-02-08 18:49:18.000000000',
message: 'test',
unresolved: true,
},
{
id: '503008e2_0ab203ee',
path: '/COMMIT_MSG',
line: 5,
in_reply_to: 'ecf0b9fa_fe1a5f62',
updated: '2018-02-13 22:48:48.018000000',
message: 'draft',
unresolved: false,
__draft: true,
__draftID: '0.m683trwff68',
__editing: false,
patch_set: '2',
},
],
patchNum: 4,
path: '/COMMIT_MSG',
line: 5,
rootId: 'ecf0b9fa_fe1a5f62',
start_datetime: '2018-02-08 18:49:18.000000000',
},
{
comments: [
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 3,
id: 'ecf0b9fa_fe5f62',
robot_id: 'rb2',
line: 5,
updated: '2018-02-08 18:49:18.000000000',
message: 'test',
unresolved: true,
},
{
__path: 'test.txt',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 3,
id: '09a9fb0a_1484e6cf',
side: 'PARENT',
updated: '2018-02-13 22:47:19.000000000',
message: 'Some comment on another patchset.',
unresolved: false,
},
],
patchNum: 3,
path: 'test.txt',
rootId: '09a9fb0a_1484e6cf',
start_datetime: '2018-02-13 22:47:19.000000000',
commentSide: 'PARENT',
},
{
comments: [
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 2,
id: '8caddf38_44770ec1',
line: 4,
updated: '2018-02-13 22:48:40.000000000',
message: 'Another unresolved comment',
unresolved: true,
},
],
patchNum: 2,
path: '/COMMIT_MSG',
line: 4,
rootId: '8caddf38_44770ec1',
start_datetime: '2018-02-13 22:48:40.000000000',
},
{
comments: [
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 2,
id: 'scaddf38_44770ec1',
line: 4,
updated: '2018-02-14 22:48:40.000000000',
message: 'Yet another unresolved comment',
unresolved: true,
},
],
patchNum: 2,
path: '/COMMIT_MSG',
line: 4,
rootId: 'scaddf38_44770ec1',
start_datetime: '2018-02-14 22:48:40.000000000',
},
{
comments: [
{
id: 'zcf0b9fa_fe1a5f62',
path: '/COMMIT_MSG',
line: 6,
updated: '2018-02-15 22:48:48.018000000',
message: 'resolved draft',
unresolved: false,
__draft: true,
__draftID: '0.m683trwff68',
__editing: false,
patch_set: '2',
},
],
patchNum: 4,
path: '/COMMIT_MSG',
line: 6,
rootId: 'zcf0b9fa_fe1a5f62',
start_datetime: '2018-02-09 18:49:18.000000000',
},
{
comments: [
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 4,
id: 'rc1',
line: 5,
updated: '2019-02-08 18:49:18.000000000',
message: 'test',
unresolved: true,
robot_id: 'rc1',
},
],
patchNum: 4,
path: '/COMMIT_MSG',
line: 5,
rootId: 'rc1',
start_datetime: '2019-02-08 18:49:18.000000000',
},
{
comments: [
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 4,
id: 'rc2',
line: 5,
updated: '2019-03-08 18:49:18.000000000',
message: 'test',
unresolved: true,
robot_id: 'rc2',
},
{
__path: '/COMMIT_MSG',
author: {
_account_id: 1000000,
name: 'user',
username: 'user',
},
patch_set: 4,
id: 'c2_1',
line: 5,
updated: '2019-03-08 18:49:18.000000000',
message: 'test',
unresolved: true,
},
],
patchNum: 4,
path: '/COMMIT_MSG',
line: 5,
rootId: 'rc2',
start_datetime: '2019-03-08 18:49:18.000000000',
},
];
setup(() => {
sandbox = sinon.sandbox.create();
stub('gr-endpoint-decorator', {
_import: sandbox.stub().returns(Promise.resolve()),
});
// Since pluginEndpoints are global, must reset state.
_testOnly_resetEndpoints();
navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange');
stub('gr-rest-api-interface', {
getConfig() { return Promise.resolve({test: 'config'}); },
getAccount() { return Promise.resolve(null); },
getDiffComments() { return Promise.resolve({}); },
getDiffRobotComments() { return Promise.resolve({}); },
getDiffDrafts() { return Promise.resolve({}); },
_fetchSharedCacheURL() { return Promise.resolve({}); },
});
element = fixture('basic');
sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
pluginLoader.loadPlugins([]);
pluginApi.install(
plugin => {
plugin.registerDynamicCustomComponent(
'change-view-tab-header',
'gr-checks-change-view-tab-header-view'
);
plugin.registerDynamicCustomComponent(
'change-view-tab-content',
'gr-checks-view'
);
},
'0.1',
'http://some/plugins/url.html'
);
});
teardown(done => {
flush(() => {
sandbox.restore();
done();
});
});
const getCustomCssValue =
cssParam => util.getComputedStyleValue(cssParam, element);
test('_handleMessageAnchorTap', () => {
element._changeNum = '1';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
const getUrlStub = sandbox.stub(GerritNav, 'getUrlForChange');
const replaceStateStub = sandbox.stub(history, 'replaceState');
element._handleMessageAnchorTap({detail: {id: 'a12345'}});
assert.equal(getUrlStub.lastCall.args[4], '#message-a12345');
assert.isTrue(replaceStateStub.called);
});
suite('plugins adding to file tab', () => {
setup(done => {
// Resolving it here instead of during setup() as other tests depend
// on flush() not being called during setup.
flush(() => done());
});
test('plugin added tab shows up as a dynamic endpoint', () => {
assert(element._dynamicTabHeaderEndpoints.includes(
'change-view-tab-header-url'));
const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
// 3 Tabs are : Files, Plugin, Findings
assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
'change-view-tab-header-url');
});
test('_setActivePrimaryTab switched tab correctly', done => {
element._setActivePrimaryTab({detail:
{tab: 'change-view-tab-header-url'}});
flush(() => {
assert.equal(element._activeTabs[0], 'change-view-tab-header-url');
done();
});
});
test('show-primary-tab switched primary tab correctly', done => {
element.dispatchEvent(
new CustomEvent('show-primary-tab', {
composed: true,
bubbles: true,
detail: {
tab: 'change-view-tab-header-url',
},
}));
flush(() => {
assert.equal(element._activeTabs[0], 'change-view-tab-header-url');
done();
});
});
test('param change should switch primary tab correctly', done => {
assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
const queryMap = new Map();
queryMap.set('tab', PrimaryTabs.FINDINGS);
// view is required
element.params = Object.assign(
{
view: GerritNav.View.CHANGE,
},
element.params, {queryMap});
flush(() => {
assert.equal(element._activeTabs[0], PrimaryTabs.FINDINGS);
done();
});
});
test('invalid param change should not switch primary tab', done => {
assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
const queryMap = new Map();
queryMap.set('tab', 'random');
// view is required
element.params = Object.assign(
{
view: GerritNav.View.CHANGE,
},
element.params, {queryMap});
flush(() => {
assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
done();
});
});
test('switching tab sets _selectedTabPluginEndpoint', done => {
const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
flush(() => {
assert.equal(element._selectedTabPluginEndpoint,
'change-view-tab-content-url');
done();
});
});
});
suite('keyboard shortcuts', () => {
test('t to add topic', () => {
const editStub = sandbox.stub(element.$.metadata, 'editTopic');
MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't');
assert(editStub.called);
});
test('S should toggle the CL star', () => {
const starStub = sandbox.stub(element.$.changeStar, 'toggleStar');
MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's');
assert(starStub.called);
});
test('U should navigate to root if no backPage set', () => {
const relativeNavStub = sandbox.stub(GerritNav,
'navigateToRelativeUrl');
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
assert.isTrue(relativeNavStub.called);
assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
GerritNav.getUrlForRoot()));
});
test('U should navigate to backPage if set', () => {
const relativeNavStub = sandbox.stub(GerritNav,
'navigateToRelativeUrl');
element.backPage = '/dashboard/self';
MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u');
assert.isTrue(relativeNavStub.called);
assert.isTrue(relativeNavStub.lastCall.calledWithExactly(
'/dashboard/self'));
});
test('A fires an error event when not logged in', done => {
sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
const loggedInErrorSpy = sandbox.spy();
element.addEventListener('show-auth-required', loggedInErrorSpy);
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isFalse(element.$.replyOverlay.opened);
assert.isTrue(loggedInErrorSpy.called);
done();
});
});
test('shift A does not open reply overlay', done => {
sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
flush(() => {
assert.isFalse(element.$.replyOverlay.opened);
done();
});
});
test('A toggles overlay when logged in', done => {
sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates')
.returns(Promise.resolve({isLatest: true}));
element._change = {labels: {}};
const openSpy = sandbox.spy(element, '_openReplyDialog');
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
flush(() => {
assert.isTrue(element.$.replyOverlay.opened);
element.$.replyOverlay.close();
assert.isFalse(element.$.replyOverlay.opened);
assert(openSpy.lastCall.calledWithExactly(
element.$.replyDialog.FocusTarget.ANY),
'_openReplyDialog should have been passed ANY');
assert.equal(openSpy.callCount, 1);
done();
});
});
test('fullscreen-overlay-opened hides content', () => {
element._loggedIn = true;
element._loading = false;
element._change = {
owner: {_account_id: 1},
labels: {},
actions: {
abandon: {
enabled: true,
label: 'Abandon',
method: 'POST',
title: 'Abandon',
},
},
};
sandbox.spy(element, '_handleHideBackgroundContent');
element.$.replyDialog.dispatchEvent(
new CustomEvent('fullscreen-overlay-opened', {
composed: true, bubbles: true,
}));
assert.isTrue(element._handleHideBackgroundContent.called);
assert.isTrue(element.$.mainContent.classList.contains('overlayOpen'));
assert.equal(getComputedStyle(element.$.actions).display, 'flex');
});
test('fullscreen-overlay-closed shows content', () => {
element._loggedIn = true;
element._loading = false;
element._change = {
owner: {_account_id: 1},
labels: {},
actions: {
abandon: {
enabled: true,
label: 'Abandon',
method: 'POST',
title: 'Abandon',
},
},
};
sandbox.spy(element, '_handleShowBackgroundContent');
element.$.replyDialog.dispatchEvent(
new CustomEvent('fullscreen-overlay-closed', {
composed: true, bubbles: true,
}));
assert.isTrue(element._handleShowBackgroundContent.called);
assert.isFalse(element.$.mainContent.classList.contains('overlayOpen'));
});
test('expand all messages when expand-diffs fired', () => {
const handleExpand =
sandbox.stub(element.$.fileList, 'expandAllDiffs');
element.$.fileListHeader.dispatchEvent(
new CustomEvent('expand-diffs', {
composed: true, bubbles: true,
}));
assert.isTrue(handleExpand.called);
});
test('collapse all messages when collapse-diffs fired', () => {
const handleCollapse =
sandbox.stub(element.$.fileList, 'collapseAllDiffs');
element.$.fileListHeader.dispatchEvent(
new CustomEvent('collapse-diffs', {
composed: true, bubbles: true,
}));
assert.isTrue(handleCollapse.called);
});
test('X should expand all messages', done => {
flush(() => {
const handleExpand = sandbox.stub(element.messagesList,
'handleExpandCollapse');
MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
assert(handleExpand.calledWith(true));
done();
});
});
test('Z should collapse all messages', done => {
flush(() => {
const handleExpand = sandbox.stub(element.messagesList,
'handleExpandCollapse');
MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
assert(handleExpand.calledWith(false));
done();
});
});
test('shift + R should fetch and navigate to the latest patch set',
done => {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
_number: 42,
revisions: {
rev1: {_number: 1, commit: {parents: []}},
},
current_revision: 'rev1',
status: 'NEW',
labels: {},
actions: {},
};
navigateToChangeStub.restore();
navigateToChangeStub = sandbox.stub(GerritNav, 'navigateToChange',
(change, patchNum, basePatchNum) => {
assert.equal(change, element._change);
assert.isUndefined(patchNum);
assert.isUndefined(basePatchNum);
done();
});
MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
});
test('d should open download overlay', () => {
const stub = sandbox.stub(element.$.downloadOverlay, 'open');
MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
assert.isTrue(stub.called);
});
test(', should open diff preferences', () => {
const stub = sandbox.stub(
element.$.fileList.$.diffPreferencesDialog, 'open');
element._loggedIn = false;
element.disableDiffPrefs = true;
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert.isFalse(stub.called);
element._loggedIn = true;
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert.isFalse(stub.called);
element.disableDiffPrefs = false;
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert.isTrue(stub.called);
});
test('m should toggle diff mode', () => {
sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
const setModeStub = sandbox.stub(element.$.fileListHeader,
'setDiffViewMode');
const e = {preventDefault: () => {}};
flushAsynchronousOperations();
element.viewState.diffMode = 'SIDE_BY_SIDE';
element._handleToggleDiffMode(e);
assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
element.viewState.diffMode = 'UNIFIED_DIFF';
element._handleToggleDiffMode(e);
assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
});
});
suite('reloading drafts', () => {
let reloadStub;
const drafts = {
'testfile.txt': [
{
patch_set: 5,
id: 'dd2982f5_c01c9e6a',
line: 1,
updated: '2017-11-08 18:47:45.000000000',
message: 'test',
unresolved: true,
},
],
};
setup(() => {
// Fake computeDraftCount as its required for ChangeComments,
// see gr-comment-api#reloadDrafts.
reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
.returns(Promise.resolve({
drafts,
getAllThreadsForChange: () => ([]),
computeDraftCount: () => 1,
}));
});
test('drafts are reloaded when reload-drafts fired', done => {
element.$.fileList.dispatchEvent(
new CustomEvent('reload-drafts', {
detail: {
resolve: () => {
assert.isTrue(reloadStub.called);
assert.deepEqual(element._diffDrafts, drafts);
done();
},
},
composed: true, bubbles: true,
}));
});
test('drafts are reloaded when comment-refresh fired', () => {
element.dispatchEvent(
new CustomEvent('comment-refresh', {
composed: true, bubbles: true,
}));
assert.isTrue(reloadStub.called);
});
});
test('diff comments modified', () => {
sandbox.spy(element, '_handleReloadCommentThreads');
return element._reloadComments().then(() => {
element.dispatchEvent(
new CustomEvent('diff-comments-modified', {
composed: true, bubbles: true,
}));
assert.isTrue(element._handleReloadCommentThreads.called);
});
});
test('thread list modified', () => {
sandbox.spy(element, '_handleReloadDiffComments');
element._activeTabs = [PrimaryTabs.FILES, SecondaryTabs.COMMENT_THREADS];
flushAsynchronousOperations();
return element._reloadComments().then(() => {
element.threadList.dispatchEvent(
new CustomEvent('thread-list-modified', {
composed: true, bubbles: true,
}));
assert.isTrue(element._handleReloadDiffComments.called);
let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
.returns(1);
assert.equal(element._computeTotalCommentCounts(5,
element._changeComments), '5 unresolved, 1 draft');
assert.equal(element._computeTotalCommentCounts(0,
element._changeComments), '1 draft');
draftStub.restore();
draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
.returns(0);
assert.equal(element._computeTotalCommentCounts(0,
element._changeComments), '');
assert.equal(element._computeTotalCommentCounts(1,
element._changeComments), '1 unresolved');
draftStub.restore();
draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
.returns(2);
assert.equal(element._computeTotalCommentCounts(1,
element._changeComments), '1 unresolved, 2 drafts');
draftStub.restore();
});
});
suite('thread list and change log tabs', () => {
setup(() => {
element._changeNum = '1';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2, commit: {parents: []}},
rev1: {_number: 1, commit: {parents: []}},
rev13: {_number: 13, commit: {parents: []}},
rev3: {_number: 3, commit: {parents: []}},
},
current_revision: 'rev3',
status: 'NEW',
labels: {
test: {
all: [],
default_value: 0,
values: [],
approved: {},
},
},
};
sandbox.stub(element.$.relatedChanges, 'reload');
sandbox.stub(element, '_reload').returns(Promise.resolve());
sandbox.spy(element, '_paramsChanged');
element.params = {view: 'change', changeNum: '1'};
});
test('tab switch works correctly', done => {
assert.isTrue(element._paramsChanged.called);
assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
const commentTab = element.shadowRoot.querySelector(
'paper-tab.commentThreads'
);
// Switch to comment thread tab
MockInteractions.tap(commentTab);
assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
// Switch back to 'Change Log' tab
element._paramsChanged(element.params);
flush(() => {
assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
done();
});
});
test('show-secondary-tab event works', () => {
assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
// Switch to comment thread tab
element.fire('show-secondary-tab', {tab: SecondaryTabs.COMMENT_THREADS});
assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
});
test('param change should switched secondary tab correctly', done => {
assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
const queryMap = new Map();
queryMap.set('secondaryTab', SecondaryTabs.COMMENT_THREADS);
// view is required
element.params = Object.assign(
{view: GerritNav.View.CHANGE},
element.params, {queryMap}
);
flush(() => {
assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
done();
});
});
test('invalid secondaryTab should not switch tab', done => {
assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
const queryMap = new Map();
queryMap.set('secondaryTab', 'random');
// view is required
element.params = Object.assign({
view: GerritNav.View.CHANGE,
}, element.params, {queryMap});
flush(() => {
assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
done();
});
});
});
suite('Findings comment tab', () => {
setup(done => {
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2, commit: {parents: []}},
rev1: {_number: 1, commit: {parents: []}},
rev13: {_number: 13, commit: {parents: []}},
rev3: {_number: 3, commit: {parents: []}},
rev4: {_number: 4, commit: {parents: []}},
},
current_revision: 'rev4',
};
element._commentThreads = THREADS;
const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
flush(() => {
done();
});
});
test('robot comments count per patchset', () => {
const count = element._robotCommentCountPerPatchSet(THREADS);
const expectedCount = {
2: 1,
3: 1,
4: 2,
};
assert.deepEqual(count, expectedCount);
assert.equal(element._computeText({_number: 2}, THREADS),
'Patchset 2 (1 finding)');
assert.equal(element._computeText({_number: 4}, THREADS),
'Patchset 4 (2 findings)');
assert.equal(element._computeText({_number: 5}, THREADS),
'Patchset 5');
});
test('only robot comments are rendered', () => {
assert.equal(element._robotCommentThreads.length, 2);
assert.equal(element._robotCommentThreads[0].comments[0].robot_id,
'rc1');
assert.equal(element._robotCommentThreads[1].comments[0].robot_id,
'rc2');
});
test('changing patchsets resets robot comments', done => {
element.set('_change.current_revision', 'rev3');
flush(() => {
assert.equal(element._robotCommentThreads.length, 1);
done();
});
});
test('Show more button is hidden', () => {
assert.isNull(element.shadowRoot.querySelector('.show-robot-comments'));
});
suite('robot comments show more button', () => {
setup(done => {
const arr = [];
for (let i = 0; i <= 30; i++) {
arr.push(...THREADS);
}
element._commentThreads = arr;
flush(() => {
done();
});
});
test('Show more button is rendered', () => {
assert.isOk(element.shadowRoot.querySelector('.show-robot-comments'));
assert.equal(element._robotCommentThreads.length,
ROBOT_COMMENTS_LIMIT);
});
test('Clicking show more button renders all comments', done => {
MockInteractions.tap(element.shadowRoot.querySelector(
'.show-robot-comments'));
flush(() => {
assert.equal(element._robotCommentThreads.length, 62);
done();
});
});
});
});
test('reply button is not visible when logged out', () => {
assert.equal(getComputedStyle(element.$.replyBtn).display, 'none');
element._loggedIn = true;
assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none');
});
test('download tap calls _handleOpenDownloadDialog', () => {
sandbox.stub(element, '_handleOpenDownloadDialog');
element.$.actions.dispatchEvent(
new CustomEvent('download-tap', {
composed: true, bubbles: true,
}));
assert.isTrue(element._handleOpenDownloadDialog.called);
});
test('fetches the server config on attached', done => {
flush(() => {
assert.equal(element._serverConfig.test, 'config');
done();
});
});
test('_changeStatuses', () => {
sandbox.stub(element, 'changeStatuses').returns(
['Merged', 'WIP']);
element._loading = false;
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev2: {_number: 2},
rev1: {_number: 1},
rev13: {_number: 13},
rev3: {_number: 3},
},
current_revision: 'rev3',
labels: {
test: {
all: [],
default_value: 0,
values: [],
approved: {},
},
},
};
element._mergeable = true;
const expectedStatuses = ['Merged', 'WIP'];
assert.deepEqual(element._changeStatuses, expectedStatuses);
assert.equal(element._changeStatus, expectedStatuses.join(', '));
flushAsynchronousOperations();
const statusChips = dom(element.root)
.querySelectorAll('gr-change-status');
assert.equal(statusChips.length, 2);
});
test('diff preferences open when open-diff-prefs is fired', () => {
const overlayOpenStub = sandbox.stub(element.$.fileList,
'openDiffPrefs');
element.$.fileListHeader.dispatchEvent(
new CustomEvent('open-diff-prefs', {
composed: true, bubbles: true,
}));
assert.isTrue(overlayOpenStub.called);
});
test('_prepareCommitMsgForLinkify', () => {
let commitMessage = 'R=test@google.com';
let result = element._prepareCommitMsgForLinkify(commitMessage);
assert.equal(result, 'R=\u200Btest@google.com');
commitMessage = 'R=test@google.com\nR=test@google.com';
result = element._prepareCommitMsgForLinkify(commitMessage);
assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com');
commitMessage = 'CC=test@google.com';
result = element._prepareCommitMsgForLinkify(commitMessage);
assert.equal(result, 'CC=\u200Btest@google.com');
}),
test('_isSubmitEnabled', () => {
assert.isFalse(element._isSubmitEnabled({}));
assert.isFalse(element._isSubmitEnabled({submit: {}}));
assert.isTrue(element._isSubmitEnabled(
{submit: {enabled: true}}));
});
test('_reload is called when an approved label is removed', () => {
const vote = {_account_id: 1, name: 'bojack', value: 1};
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
owner: {email: 'abc@def'},
revisions: {
rev2: {_number: 2, commit: {parents: []}},
rev1: {_number: 1, commit: {parents: []}},
rev13: {_number: 13, commit: {parents: []}},
rev3: {_number: 3, commit: {parents: []}},
},
current_revision: 'rev3',
status: 'NEW',
labels: {
test: {
all: [vote],
default_value: 0,
values: [],
approved: {},
},
},
};
flushAsynchronousOperations();
const reloadStub = sandbox.stub(element, '_reload');
element.splice('_change.labels.test.all', 0, 1);
assert.isFalse(reloadStub.called);
element._change.labels.test.all.push(vote);
element._change.labels.test.all.push(vote);
element._change.labels.test.approved = vote;
flushAsynchronousOperations();
element.splice('_change.labels.test.all', 0, 2);
assert.isTrue(reloadStub.called);
assert.isTrue(reloadStub.calledOnce);
});
test('reply button has updated count when there are drafts', () => {
const getLabel = element._computeReplyButtonLabel;
assert.equal(getLabel(null, false), 'Reply');
assert.equal(getLabel(null, true), 'Start Review');
const changeRecord = {base: null};
assert.equal(getLabel(changeRecord, false), 'Reply');
changeRecord.base = {};
assert.equal(getLabel(changeRecord, false), 'Reply');
changeRecord.base = {
'file1.txt': [{}],
'file2.txt': [{}, {}],
};
assert.equal(getLabel(changeRecord, false), 'Reply (3)');
});
test('comment events properly update diff drafts', () => {
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
const draft = {
__draft: true,
id: 'id1',
path: '/foo/bar.txt',
text: 'hello',
};
element._handleCommentSave({detail: {comment: draft}});
draft.patch_set = 2;
assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
draft.patch_set = null;
draft.text = 'hello, there';
element._handleCommentSave({detail: {comment: draft}});
draft.patch_set = 2;
assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]});
const draft2 = {
__draft: true,
id: 'id2',
path: '/foo/bar.txt',
text: 'hola',
};
element._handleCommentSave({detail: {comment: draft2}});
draft2.patch_set = 2;
assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]});
draft.patch_set = null;
element._handleCommentDiscard({detail: {comment: draft}});
draft.patch_set = 2;
assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]});
element._handleCommentDiscard({detail: {comment: draft2}});
assert.deepEqual(element._diffDrafts, {});
});
test('change num change', () => {
element._changeNum = null;
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
labels: {},
};
element.viewState.changeNum = null;
element.viewState.diffMode = 'UNIFIED';
assert.equal(element.viewState.numFilesShown, 200);
assert.equal(element._numFilesShown, 200);
element._numFilesShown = 150;
flushAsynchronousOperations();
assert.equal(element.viewState.diffMode, 'UNIFIED');
assert.equal(element.viewState.numFilesShown, 150);
element._changeNum = '1';
element.params = {changeNum: '1'};
element._change.newProp = '1';
flushAsynchronousOperations();
assert.equal(element.viewState.diffMode, 'UNIFIED');
assert.equal(element.viewState.changeNum, '1');
element._changeNum = '2';
element.params = {changeNum: '2'};
element._change.newProp = '2';
flushAsynchronousOperations();
assert.equal(element.viewState.diffMode, 'UNIFIED');
assert.equal(element.viewState.changeNum, '2');
assert.equal(element.viewState.numFilesShown, 200);
assert.equal(element._numFilesShown, 200);
});
test('_setDiffViewMode is called with reset when new change is loaded',
() => {
sandbox.stub(element, '_setDiffViewMode');
element.viewState = {changeNum: 1};
element._changeNum = 2;
element._resetFileListViewState();
assert.isTrue(
element._setDiffViewMode.lastCall.calledWithExactly(true));
});
test('diffViewMode is propagated from file list header', () => {
element.viewState = {diffMode: 'UNIFIED'};
element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE';
assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
});
test('diffMode defaults to side by side without preferences', done => {
sandbox.stub(element.$.restAPI, 'getPreferences').returns(
Promise.resolve({}));
// No user prefs or diff view mode set.
element._setDiffViewMode().then(() => {
assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
done();
});
});
test('diffMode defaults to preference when not already set', done => {
sandbox.stub(element.$.restAPI, 'getPreferences').returns(
Promise.resolve({default_diff_view: 'UNIFIED'}));
element._setDiffViewMode().then(() => {
assert.equal(element.viewState.diffMode, 'UNIFIED');
done();
});
});
test('existing diffMode overrides preference', done => {
element.viewState.diffMode = 'SIDE_BY_SIDE';
sandbox.stub(element.$.restAPI, 'getPreferences').returns(
Promise.resolve({default_diff_view: 'UNIFIED'}));
element._setDiffViewMode().then(() => {
assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE');
done();
});
});
test('don’t reload entire page when patchRange changes', () => {
const reloadStub = sandbox.stub(element, '_reload',
() => Promise.resolve());
const reloadPatchDependentStub = sandbox.stub(element,
'_reloadPatchNumDependentResources',
() => Promise.resolve());
const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear');
const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
const value = {
view: GerritNav.View.CHANGE,
patchNum: '1',
};
element._paramsChanged(value);
assert.isTrue(reloadStub.calledOnce);
assert.isTrue(relatedClearSpy.calledOnce);
element._initialLoadComplete = true;
value.basePatchNum = '1';
value.patchNum = '2';
element._paramsChanged(value);
assert.isFalse(reloadStub.calledTwice);
assert.isTrue(reloadPatchDependentStub.calledOnce);
assert.isTrue(relatedClearSpy.calledOnce);
assert.isTrue(collapseStub.calledTwice);
});
test('reload entire page when patchRange doesnt change', () => {
const reloadStub = sandbox.stub(element, '_reload',
() => Promise.resolve());
const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs');
const value = {
view: GerritNav.View.CHANGE,
};
element._paramsChanged(value);
assert.isTrue(reloadStub.calledOnce);
element._initialLoadComplete = true;
element._paramsChanged(value);
assert.isTrue(reloadStub.calledTwice);
assert.isTrue(collapseStub.calledTwice);
});
test('related changes are updated and new patch selected after rebase',
done => {
element._changeNum = '42';
sandbox.stub(element, 'computeLatestPatchNum', () => 1);
sandbox.stub(element, '_reload',
() => Promise.resolve());
const e = {detail: {action: 'rebase'}};
element._handleReloadChange(e).then(() => {
assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly(
element._change));
done();
});
});
test('related changes are not updated after other action', done => {
sandbox.stub(element, '_reload', () => Promise.resolve());
sandbox.stub(element.$.relatedChanges, 'reload');
const e = {detail: {action: 'abandon'}};
element._handleReloadChange(e).then(() => {
assert.isFalse(navigateToChangeStub.called);
done();
});
});
test('_computeMergedCommitInfo', () => {
const dummyRevs = {
1: {commit: {commit: 1}},
2: {commit: {}},
};
assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {});
assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs),
dummyRevs[1].commit);
// Regression test for issue 5337.
const commit = element._computeMergedCommitInfo(2, dummyRevs);
assert.notDeepEqual(commit, dummyRevs[2]);
assert.deepEqual(commit, {commit: 2});
});
test('_computeCopyTextForTitle', () => {
const change = {
_number: 123,
subject: 'test subject',
revisions: {
rev1: {_number: 1},
rev3: {_number: 3},
},
current_revision: 'rev3',
};
sandbox.stub(GerritNav, 'getUrlForChange')
.returns('/change/123');
assert.equal(
element._computeCopyTextForTitle(change),
`123: test subject | http://${location.host}/change/123`
);
});
test('get latest revision', () => {
let change = {
revisions: {
rev1: {_number: 1},
rev3: {_number: 3},
},
current_revision: 'rev3',
};
assert.equal(element._getLatestRevisionSHA(change), 'rev3');
change = {
revisions: {
rev1: {_number: 1},
},
};
assert.equal(element._getLatestRevisionSHA(change), 'rev1');
});
test('show commit message edit button', () => {
const _change = {
status: element.ChangeStatus.MERGED,
};
assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
assert.isTrue(element._computeHideEditCommitMessage(false, true, {}));
assert.isFalse(element._computeHideEditCommitMessage(true, false, {}));
assert.isTrue(element._computeHideEditCommitMessage(true, false,
_change));
assert.isTrue(element._computeHideEditCommitMessage(true, false, {},
true));
assert.isFalse(element._computeHideEditCommitMessage(true, false, {},
false));
});
test('_handleCommitMessageSave trims trailing whitespace', () => {
const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage')
.returns(Promise.resolve({}));
const mockEvent = content => { return {detail: {content}}; };
element._handleCommitMessageSave(mockEvent('test \n test '));
assert.equal(putStub.lastCall.args[1], 'test\n test');
element._handleCommitMessageSave(mockEvent(' test\ntest'));
assert.equal(putStub.lastCall.args[1], ' test\ntest');
element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n'));
assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n');
});
test('_computeChangeIdCommitMessageError', () => {
let commitMessage =
'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483';
let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
null);
change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
'mismatch');
commitMessage = 'This is the greatest change.';
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
'missing');
});
test('multiple change Ids in commit message picks last', () => {
const commitMessage = [
'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
].join('\n');
let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
null);
change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
'mismatch');
});
test('does not count change Id that starts mid line', () => {
const commitMessage = [
'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484',
'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483',
].join(' and ');
let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'};
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
null);
change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'};
assert.equal(
element._computeChangeIdCommitMessageError(commitMessage, change),
'mismatch');
});
test('_computeTitleAttributeWarning', () => {
let changeIdCommitMessageError = 'missing';
assert.equal(
element._computeTitleAttributeWarning(changeIdCommitMessageError),
'No Change-Id in commit message');
changeIdCommitMessageError = 'mismatch';
assert.equal(
element._computeTitleAttributeWarning(changeIdCommitMessageError),
'Change-Id mismatch');
});
test('_computeChangeIdClass', () => {
let changeIdCommitMessageError = 'missing';
assert.equal(
element._computeChangeIdClass(changeIdCommitMessageError), '');
changeIdCommitMessageError = 'mismatch';
assert.equal(
element._computeChangeIdClass(changeIdCommitMessageError), 'warning');
});
test('topic is coalesced to null', done => {
sandbox.stub(element, '_changeChanged');
sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
id: '123456789',
labels: {},
current_revision: 'foo',
revisions: {foo: {commit: {}}},
}));
element._getChangeDetail().then(() => {
assert.isNull(element._change.topic);
done();
});
});
test('commit sha is populated from getChangeDetail', done => {
sandbox.stub(element, '_changeChanged');
sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
id: '123456789',
labels: {},
current_revision: 'foo',
revisions: {foo: {commit: {}}},
}));
element._getChangeDetail().then(() => {
assert.equal('foo', element._commitInfo.commit);
done();
});
});
test('edit is added to change', () => {
sandbox.stub(element, '_changeChanged');
sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({
id: '123456789',
labels: {},
current_revision: 'foo',
revisions: {foo: {commit: {}}},
}));
sandbox.stub(element, '_getEdit', () => Promise.resolve({
base_patch_set_number: 1,
commit: {commit: 'bar'},
}));
element._patchRange = {};
return element._getChangeDetail().then(() => {
const revs = element._change.revisions;
assert.equal(Object.keys(revs).length, 2);
assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}});
assert.deepEqual(revs['bar'], {
_number: element.EDIT_NAME,
basePatchNum: 1,
commit: {commit: 'bar'},
fetch: undefined,
});
});
});
test('_getBasePatchNum', () => {
const _change = {
_number: 42,
revisions: {
'98da160735fb81604b4c40e93c368f380539dd0e': {
_number: 1,
commit: {
parents: [],
},
},
},
};
const _patchRange = {
basePatchNum: 'PARENT',
};
assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT');
element._prefs = {
default_base_for_merges: 'FIRST_PARENT',
};
const _change2 = {
_number: 42,
revisions: {
'98da160735fb81604b4c40e93c368f380539dd0e': {
_number: 1,
commit: {
parents: [
{
commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8',
subject: 'test',
},
{
commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841',
subject: 'test3',
},
],
},
},
},
};
assert.equal(element._getBasePatchNum(_change2, _patchRange), -1);
_patchRange.patchNum = 1;
assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT');
});
test('_openReplyDialog called with `ANY` when coming from tap event',
() => {
const openStub = sandbox.stub(element, '_openReplyDialog');
element._serverConfig = {};
MockInteractions.tap(element.$.replyBtn);
assert(openStub.lastCall.calledWithExactly(
element.$.replyDialog.FocusTarget.ANY),
'_openReplyDialog should have been passed ANY');
assert.equal(openStub.callCount, 1);
});
test('_openReplyDialog called with `BODY` when coming from message reply' +
'event', done => {
flush(() => {
const openStub = sandbox.stub(element, '_openReplyDialog');
element.messagesList.dispatchEvent(
new CustomEvent('reply', {
detail:
{message: {message: 'text'}},
composed: true, bubbles: true,
}));
assert(openStub.lastCall.calledWithExactly(
element.$.replyDialog.FocusTarget.BODY),
'_openReplyDialog should have been passed BODY');
assert.equal(openStub.callCount, 1);
done();
});
});
test('reply dialog focus can be controlled', () => {
const FocusTarget = element.$.replyDialog.FocusTarget;
const openStub = sandbox.stub(element, '_openReplyDialog');
const e = {detail: {}};
element._handleShowReplyDialog(e);
assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
'_openReplyDialog should have been passed REVIEWERS');
assert.equal(openStub.callCount, 1);
e.detail.value = {ccsOnly: true};
element._handleShowReplyDialog(e);
assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
'_openReplyDialog should have been passed CCS');
assert.equal(openStub.callCount, 2);
});
test('getUrlParameter functionality', () => {
const locationStub = sandbox.stub(element, '_getLocationSearch');
locationStub.returns('?test');
assert.equal(element._getUrlParameter('test'), 'test');
locationStub.returns('?test2=12&test=3');
assert.equal(element._getUrlParameter('test'), 'test');
locationStub.returns('');
assert.isNull(element._getUrlParameter('test'));
locationStub.returns('?');
assert.isNull(element._getUrlParameter('test'));
locationStub.returns('?test2');
assert.isNull(element._getUrlParameter('test'));
});
test('revert dialog opened with revert param', done => {
sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true));
sandbox.stub(pluginLoader, 'awaitPluginsLoaded', () => Promise.resolve());
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 2,
};
element._change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
revisions: {
rev1: {_number: 1, commit: {parents: []}},
rev2: {_number: 2, commit: {parents: []}},
},
current_revision: 'rev1',
status: element.ChangeStatus.MERGED,
labels: {},
actions: {},
};
sandbox.stub(element, '_getUrlParameter',
param => {
assert.equal(param, 'revert');
return param;
});
sandbox.stub(element.$.actions, 'showRevertDialog',
done);
element._maybeShowRevertDialog();
assert.isTrue(pluginLoader.awaitPluginsLoaded.called);
});
suite('scroll related tests', () => {
test('document scrolling calls function to set scroll height', done => {
const originalHeight = document.body.scrollHeight;
const scrollStub = sandbox.stub(element, '_handleScroll',
() => {
assert.isTrue(scrollStub.called);
document.body.style.height = originalHeight + 'px';
scrollStub.restore();
done();
});
document.body.style.height = '10000px';
element._handleScroll();
});
test('scrollTop is set correctly', () => {
element.viewState = {scrollTop: TEST_SCROLL_TOP_PX};
sandbox.stub(element, '_reload', () => {
// When element is reloaded, ensure that the history
// state has the scrollTop set earlier. This will then
// be reset.
assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX);
return Promise.resolve({});
});
// simulate reloading component, which is done when route
// changes to match a regex of change view type.
element._paramsChanged({view: GerritNav.View.CHANGE});
});
test('scrollTop is reset when new change is loaded', () => {
element._resetFileListViewState();
assert.equal(element.viewState.scrollTop, 0);
});
});
suite('reply dialog tests', () => {
setup(() => {
sandbox.stub(element.$.replyDialog, '_draftChanged');
sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates',
() => Promise.resolve({isLatest: true}));
element._change = {labels: {}};
});
test('reply from comment adds quote text', () => {
const e = {detail: {message: {message: 'quote text'}}};
element._handleMessageReply(e);
assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
});
test('reply from comment replaces quote text', () => {
element.$.replyDialog.draft = '> old quote text\n\n some draft text';
element.$.replyDialog.quote = '> old quote text\n\n';
const e = {detail: {message: {message: 'quote text'}}};
element._handleMessageReply(e);
assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
});
test('reply from same comment preserves quote text', () => {
element.$.replyDialog.draft = '> quote text\n\n some draft text';
element.$.replyDialog.quote = '> quote text\n\n';
const e = {detail: {message: {message: 'quote text'}}};
element._handleMessageReply(e);
assert.equal(element.$.replyDialog.draft,
'> quote text\n\n some draft text');
assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
});
test('reply from top of page contains previous draft', () => {
const div = document.createElement('div');
element.$.replyDialog.draft = '> quote text\n\n some draft text';
element.$.replyDialog.quote = '> quote text\n\n';
const e = {target: div, preventDefault: sandbox.spy()};
element._handleReplyTap(e);
assert.equal(element.$.replyDialog.draft,
'> quote text\n\n some draft text');
assert.equal(element.$.replyDialog.quote, '> quote text\n\n');
});
});
test('reply button is disabled until server config is loaded', () => {
assert.isTrue(element._replyDisabled);
element._serverConfig = {};
assert.isFalse(element._replyDisabled);
});
suite('commit message expand/collapse', () => {
setup(() => {
sandbox.stub(element, 'fetchChangeUpdates',
() => Promise.resolve({isLatest: false}));
});
test('commitCollapseToggle hidden for short commit message', () => {
element._latestCommitMessage = '';
assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden'));
});
test('commitCollapseToggle shown for long commit message', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden'));
});
test('commitCollapseToggle functions', () => {
element._latestCommitMessage = _.times(35, String).join('\n');
assert.isTrue(element._commitCollapsed);
assert.isTrue(element._commitCollapsible);
assert.isTrue(
element.$.commitMessageEditor.hasAttribute('collapsed'));
MockInteractions.tap(element.$.commitCollapseToggleButton);
assert.isFalse(element._commitCollapsed);
assert.isTrue(element._commitCollapsible);
assert.isFalse(
element.$.commitMessageEditor.hasAttribute('collapsed'));
});
});
suite('related changes expand/collapse', () => {
let updateHeightSpy;
setup(() => {
updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight');
});
test('relatedChangesToggle shown height greater than changeInfo height',
() => {
assert.isFalse(element.$.relatedChangesToggle.classList
.contains('showToggle'));
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(element, '_getScrollHeight', () => 60);
sandbox.stub(element, '_getLineHeight', () => 5);
sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
element.$.relatedChanges.dispatchEvent(
new CustomEvent('new-section-loaded'));
assert.isTrue(element.$.relatedChangesToggle.classList
.contains('showToggle'));
assert.equal(updateHeightSpy.callCount, 1);
});
test('relatedChangesToggle hidden height less than changeInfo height',
() => {
assert.isFalse(element.$.relatedChangesToggle.classList
.contains('showToggle'));
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(element, '_getScrollHeight', () => 40);
sandbox.stub(element, '_getLineHeight', () => 5);
sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
element.$.relatedChanges.dispatchEvent(
new CustomEvent('new-section-loaded'));
assert.isFalse(element.$.relatedChangesToggle.classList
.contains('showToggle'));
assert.equal(updateHeightSpy.callCount, 1);
});
test('relatedChangesToggle functions', () => {
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
element._relatedChangesLoading = false;
assert.isTrue(element._relatedChangesCollapsed);
assert.isTrue(
element.$.relatedChanges.classList.contains('collapsed'));
MockInteractions.tap(element.$.relatedChangesToggleButton);
assert.isFalse(element._relatedChangesCollapsed);
assert.isFalse(
element.$.relatedChanges.classList.contains('collapsed'));
});
test('_updateRelatedChangeMaxHeight without commit toggle', () => {
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(element, '_getLineHeight', () => 12);
sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
// 50 (existing height) - 30 (extra height) = 20 (adjusted height).
// 20 (max existing height) % 12 (line height) = 6 (remainder).
// 20 (adjusted height) - 8 (remainder) = 12 (max height to set).
element._updateRelatedChangeMaxHeight();
assert.equal(getCustomCssValue('--relation-chain-max-height'),
'12px');
assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
'');
});
test('_updateRelatedChangeMaxHeight with commit toggle', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(element, '_getLineHeight', () => 12);
sandbox.stub(window, 'matchMedia', () => { return {matches: false}; });
// 50 (existing height) % 12 (line height) = 2 (remainder).
// 50 (existing height) - 2 (remainder) = 48 (max height to set).
element._updateRelatedChangeMaxHeight();
assert.equal(getCustomCssValue('--relation-chain-max-height'),
'48px');
assert.equal(getCustomCssValue('--related-change-btn-top-padding'),
'2px');
});
test('_updateRelatedChangeMaxHeight in small screen mode', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(element, '_getLineHeight', () => 12);
sandbox.stub(window, 'matchMedia', () => { return {matches: true}; });
element._updateRelatedChangeMaxHeight();
// 400 (new height) % 12 (line height) = 4 (remainder).
// 400 (new height) - 4 (remainder) = 396.
assert.equal(getCustomCssValue('--relation-chain-max-height'),
'396px');
});
test('_updateRelatedChangeMaxHeight in medium screen mode', () => {
element._latestCommitMessage = _.times(31, String).join('\n');
sandbox.stub(element, '_getOffsetHeight', () => 50);
sandbox.stub(element, '_getLineHeight', () => 12);
sandbox.stub(window, 'matchMedia', () => {
if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') {
return {matches: true};
} else {
return {matches: false};
}
});
// 100 (new height) % 12 (line height) = 4 (remainder).
// 100 (new height) - 4 (remainder) = 96.
element._updateRelatedChangeMaxHeight();
assert.equal(getCustomCssValue('--relation-chain-max-height'),
'96px');
});
suite('update checks', () => {
setup(() => {
sandbox.spy(element, '_startUpdateCheckTimer');
sandbox.stub(element, 'async', f => {
// Only fire the async callback one time.
if (element.async.callCount > 1) { return; }
f.call(element);
});
});
test('_startUpdateCheckTimer negative delay', () => {
sandbox.stub(element, 'fetchChangeUpdates');
element._serverConfig = {change: {update_delay: -1}};
assert.isTrue(element._startUpdateCheckTimer.called);
assert.isFalse(element.fetchChangeUpdates.called);
});
test('_startUpdateCheckTimer up-to-date', () => {
sandbox.stub(element, 'fetchChangeUpdates',
() => Promise.resolve({isLatest: true}));
element._serverConfig = {change: {update_delay: 12345}};
assert.isTrue(element._startUpdateCheckTimer.called);
assert.isTrue(element.fetchChangeUpdates.called);
assert.equal(element.async.lastCall.args[1], 12345 * 1000);
});
test('_startUpdateCheckTimer out-of-date shows an alert', done => {
sandbox.stub(element, 'fetchChangeUpdates',
() => Promise.resolve({isLatest: false}));
element.addEventListener('show-alert', e => {
assert.equal(e.detail.message,
'A newer patch set has been uploaded');
done();
});
element._serverConfig = {change: {update_delay: 12345}};
});
test('_startUpdateCheckTimer new status shows an alert', done => {
sandbox.stub(element, 'fetchChangeUpdates')
.returns(Promise.resolve({
isLatest: true,
newStatus: element.ChangeStatus.MERGED,
}));
element.addEventListener('show-alert', e => {
assert.equal(e.detail.message, 'This change has been merged');
done();
});
element._serverConfig = {change: {update_delay: 12345}};
});
test('_startUpdateCheckTimer new messages shows an alert', done => {
sandbox.stub(element, 'fetchChangeUpdates')
.returns(Promise.resolve({
isLatest: true,
newMessages: true,
}));
element.addEventListener('show-alert', e => {
assert.equal(e.detail.message,
'There are new messages on this change');
done();
});
element._serverConfig = {change: {update_delay: 12345}};
});
});
test('canStartReview computation', () => {
const change1 = {};
const change2 = {
actions: {
ready: {
enabled: true,
},
},
};
const change3 = {
actions: {
ready: {
label: 'Ready for Review',
},
},
};
assert.isFalse(element._computeCanStartReview(change1));
assert.isTrue(element._computeCanStartReview(change2));
assert.isFalse(element._computeCanStartReview(change3));
});
});
test('header class computation', () => {
assert.equal(element._computeHeaderClass(), 'header');
assert.equal(element._computeHeaderClass(true), 'header editMode');
});
test('_maybeScrollToMessage', done => {
flush(() => {
const scrollStub = sandbox.stub(element.messagesList,
'scrollToMessage');
element._maybeScrollToMessage('');
assert.isFalse(scrollStub.called);
element._maybeScrollToMessage('message');
assert.isFalse(scrollStub.called);
element._maybeScrollToMessage('#message-TEST');
assert.isTrue(scrollStub.called);
assert.equal(scrollStub.lastCall.args[0], 'TEST');
done();
});
});
test('topic update reloads related changes', () => {
sandbox.stub(element.$.relatedChanges, 'reload');
element.dispatchEvent(new CustomEvent('topic-changed'));
assert.isTrue(element.$.relatedChanges.reload.calledOnce);
});
test('_computeEditMode', () => {
const callCompute = (range, params) =>
element._computeEditMode({base: range}, {base: params});
assert.isFalse(callCompute({}, {}));
assert.isTrue(callCompute({}, {edit: true}));
assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {}));
assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {}));
assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {}));
});
test('_processEdit', () => {
element._patchRange = {};
const change = {
current_revision: 'foo',
revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}},
};
let mockChange;
// With no edit, mockChange should be unmodified.
element._processEdit(mockChange = _.cloneDeep(change), null);
assert.deepEqual(mockChange, change);
// When edit is not based on the latest PS, current_revision should be
// unmodified.
const edit = {
base_patch_set_number: 1,
commit: {commit: 'bar'},
fetch: true,
};
element._processEdit(mockChange = _.cloneDeep(change), edit);
assert.notDeepEqual(mockChange, change);
assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME);
assert.equal(mockChange.current_revision, change.current_revision);
assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'});
assert.notOk(mockChange.revisions.bar.actions);
edit.base_revision = 'foo';
element._processEdit(mockChange = _.cloneDeep(change), edit);
assert.notDeepEqual(mockChange, change);
assert.equal(mockChange.current_revision, 'bar');
assert.deepEqual(mockChange.revisions.bar.actions,
mockChange.revisions.foo.actions);
// If _patchRange.patchNum is defined, do not load edit.
element._patchRange.patchNum = 'baz';
change.current_revision = 'baz';
element._processEdit(mockChange = _.cloneDeep(change), edit);
assert.equal(element._patchRange.patchNum, 'baz');
assert.notOk(mockChange.revisions.bar.actions);
});
test('file-action-tap handling', () => {
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
const fileList = element.$.fileList;
const Actions = GrEditConstants.Actions;
const controls = element.$.fileListHeader.$.editControls;
sandbox.stub(controls, 'openDeleteDialog');
sandbox.stub(controls, 'openRenameDialog');
sandbox.stub(controls, 'openRestoreDialog');
sandbox.stub(GerritNav, 'getEditUrlForDiff');
sandbox.stub(GerritNav, 'navigateToRelativeUrl');
// Delete
fileList.dispatchEvent(new CustomEvent('file-action-tap', {
detail: {action: Actions.DELETE.id, path: 'foo'},
bubbles: true,
composed: true,
}));
flushAsynchronousOperations();
assert.isTrue(controls.openDeleteDialog.called);
assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
// Restore
fileList.dispatchEvent(new CustomEvent('file-action-tap', {
detail: {action: Actions.RESTORE.id, path: 'foo'},
bubbles: true,
composed: true,
}));
flushAsynchronousOperations();
assert.isTrue(controls.openRestoreDialog.called);
assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
// Rename
fileList.dispatchEvent(new CustomEvent('file-action-tap', {
detail: {action: Actions.RENAME.id, path: 'foo'},
bubbles: true,
composed: true,
}));
flushAsynchronousOperations();
assert.isTrue(controls.openRenameDialog.called);
assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
// Open
fileList.dispatchEvent(new CustomEvent('file-action-tap', {
detail: {action: Actions.OPEN.id, path: 'foo'},
bubbles: true,
composed: true,
}));
flushAsynchronousOperations();
assert.isTrue(GerritNav.getEditUrlForDiff.called);
assert.equal(GerritNav.getEditUrlForDiff.lastCall.args[1], 'foo');
assert.equal(GerritNav.getEditUrlForDiff.lastCall.args[2], '1');
assert.isTrue(GerritNav.navigateToRelativeUrl.called);
});
test('_selectedRevision updates when patchNum is changed', () => {
const revision1 = {_number: 1, commit: {parents: []}};
const revision2 = {_number: 2, commit: {parents: []}};
sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
Promise.resolve({
revisions: {
aaa: revision1,
bbb: revision2,
},
labels: {},
actions: {},
current_revision: 'bbb',
change_id: 'loremipsumdolorsitamet',
}));
sandbox.stub(element, '_getEdit').returns(Promise.resolve());
sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
element._patchRange = {patchNum: '2'};
return element._getChangeDetail().then(() => {
assert.strictEqual(element._selectedRevision, revision2);
element.set('_patchRange.patchNum', '1');
assert.strictEqual(element._selectedRevision, revision1);
});
});
test('_selectedRevision is assigned when patchNum is edit', () => {
const revision1 = {_number: 1, commit: {parents: []}};
const revision2 = {_number: 2, commit: {parents: []}};
const revision3 = {_number: 'edit', commit: {parents: []}};
sandbox.stub(element.$.restAPI, 'getChangeDetail').returns(
Promise.resolve({
revisions: {
aaa: revision1,
bbb: revision2,
ccc: revision3,
},
labels: {},
actions: {},
current_revision: 'ccc',
change_id: 'loremipsumdolorsitamet',
}));
sandbox.stub(element, '_getEdit').returns(Promise.resolve());
sandbox.stub(element, '_getPreferences').returns(Promise.resolve({}));
element._patchRange = {patchNum: 'edit'};
return element._getChangeDetail().then(() => {
assert.strictEqual(element._selectedRevision, revision3);
});
});
test('_sendShowChangeEvent', () => {
element._change = {labels: {}};
element._patchRange = {patchNum: 4};
element._mergeable = true;
const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent');
element._sendShowChangeEvent();
assert.isTrue(showStub.calledOnce);
assert.equal(
showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE);
assert.deepEqual(showStub.lastCall.args[1], {
change: {labels: {}},
patchNum: 4,
info: {mergeable: true},
});
});
suite('_handleEditTap', () => {
let fireEdit;
setup(() => {
fireEdit = () => {
element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
};
navigateToChangeStub.restore();
element._change = {revisions: {rev1: {_number: 1}}};
});
test('edit exists in revisions', done => {
sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
assert.equal(args.length, 2);
assert.equal(args[1], element.EDIT_NAME); // patchNum
done();
});
element.set('_change.revisions.rev2', {_number: element.EDIT_NAME});
flushAsynchronousOperations();
fireEdit();
});
test('no edit exists in revisions, non-latest patchset', done => {
sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
assert.equal(args.length, 4);
assert.equal(args[1], 1); // patchNum
assert.equal(args[3], true); // opt_isEdit
done();
});
element.set('_change.revisions.rev2', {_number: 2});
element._patchRange = {patchNum: 1};
flushAsynchronousOperations();
fireEdit();
});
test('no edit exists in revisions, latest patchset', done => {
sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
assert.equal(args.length, 4);
// No patch should be specified when patchNum == latest.
assert.isNotOk(args[1]); // patchNum
assert.equal(args[3], true); // opt_isEdit
done();
});
element.set('_change.revisions.rev2', {_number: 2});
element._patchRange = {patchNum: 2};
flushAsynchronousOperations();
fireEdit();
});
});
test('_handleStopEditTap', done => {
sandbox.stub(element.$.metadata, '_computeLabelNames');
navigateToChangeStub.restore();
sandbox.stub(element, 'computeLatestPatchNum').returns(1);
sandbox.stub(GerritNav, 'navigateToChange', (...args) => {
assert.equal(args.length, 2);
assert.equal(args[1], 1); // patchNum
done();
});
element._patchRange = {patchNum: 1};
element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap',
{bubbles: false}));
});
suite('plugin endpoints', () => {
test('endpoint params', done => {
element._change = {labels: {}};
element._selectedRevision = {};
let hookEl;
let plugin;
pluginApi.install(
p => {
plugin = p;
plugin.hook('change-view-integration').getLastAttached()
.then(
el => hookEl = el);
},
'0.1',
'http://some/plugins/url.html');
flush(() => {
assert.strictEqual(hookEl.plugin, plugin);
assert.strictEqual(hookEl.change, element._change);
assert.strictEqual(hookEl.revision, element._selectedRevision);
done();
});
});
});
suite('_getMergeability', () => {
let getMergeableStub;
setup(() => {
element._change = {labels: {}};
getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable')
.returns(Promise.resolve({mergeable: true}));
});
test('merged change', () => {
element._mergeable = null;
element._change.status = element.ChangeStatus.MERGED;
return element._getMergeability().then(() => {
assert.isFalse(element._mergeable);
assert.isFalse(getMergeableStub.called);
});
});
test('abandoned change', () => {
element._mergeable = null;
element._change.status = element.ChangeStatus.ABANDONED;
return element._getMergeability().then(() => {
assert.isFalse(element._mergeable);
assert.isFalse(getMergeableStub.called);
});
});
test('open change', () => {
element._mergeable = null;
return element._getMergeability().then(() => {
assert.isTrue(element._mergeable);
assert.isTrue(getMergeableStub.called);
});
});
});
test('_paramsChanged sets in projectLookup', () => {
sandbox.stub(element.$.relatedChanges, 'reload');
sandbox.stub(element, '_reload').returns(Promise.resolve());
const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
element._paramsChanged({
view: GerritNav.View.CHANGE,
changeNum: 101,
project: 'test-project',
});
assert.isTrue(setStub.calledOnce);
assert.isTrue(setStub.calledWith(101, 'test-project'));
});
test('_handleToggleStar called when star is tapped', () => {
element._change = {
owner: {_account_id: 1},
starred: false,
};
element._loggedIn = true;
const stub = sandbox.stub(element, '_handleToggleStar');
flushAsynchronousOperations();
MockInteractions.tap(element.$.changeStar.shadowRoot
.querySelector('button'));
assert.isTrue(stub.called);
});
suite('gr-reporting tests', () => {
setup(() => {
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: 1,
};
sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve());
sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve());
sandbox.stub(element, '_reloadComments').returns(Promise.resolve());
sandbox.stub(element, '_getMergeability').returns(Promise.resolve());
sandbox.stub(element, '_getLatestCommitMessage')
.returns(Promise.resolve());
});
test('don\'t report changedDisplayed on reply', done => {
const changeDisplayStub =
sandbox.stub(element.$.reporting, 'changeDisplayed');
const changeFullyLoadedStub =
sandbox.stub(element.$.reporting, 'changeFullyLoaded');
element._handleReplySent();
flush(() => {
assert.isFalse(changeDisplayStub.called);
assert.isFalse(changeFullyLoadedStub.called);
done();
});
});
test('report changedDisplayed on _paramsChanged', done => {
const changeDisplayStub =
sandbox.stub(element.$.reporting, 'changeDisplayed');
const changeFullyLoadedStub =
sandbox.stub(element.$.reporting, 'changeFullyLoaded');
element._paramsChanged({
view: GerritNav.View.CHANGE,
changeNum: 101,
project: 'test-project',
});
flush(() => {
assert.isTrue(changeDisplayStub.called);
assert.isTrue(changeFullyLoadedStub.called);
done();
});
});
});
});
</script>