blob: a21ddc31d85d6bfe8fe3ed68e9ba46ba49a8f063 [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-js-api-interface';
import {GrPopupInterface} from '../../plugins/gr-popup-interface/gr-popup-interface';
import {EventType} from '../../../api/plugin';
import {PLUGIN_LOADING_TIMEOUT_MS} from './gr-api-utils';
import {
stubRestApi,
stubBaseUrl,
waitEventLoop,
waitUntilCalled,
assertFails,
} from '../../../test/test-utils';
import {assert} from '@open-wc/testing';
import {testResolver} from '../../../test/common-test-setup';
import {PluginLoader, pluginLoaderToken} from './gr-plugin-loader';
import {useFakeTimers, stub, SinonFakeTimers, SinonStub} from 'sinon';
import {GrJsApiInterface} from './gr-js-api-interface-element';
import {Plugin} from './gr-public-js-api';
import {
ChangeInfo,
HttpMethod,
NumericChangeId,
PatchSetNum,
RevisionPatchSetNum,
Timestamp,
} from '../../../api/rest-api';
import {ParsedChangeInfo} from '../../../types/types';
import {
createChange,
createParsedChange,
createRevision,
} from '../../../test/test-data-generators';
import {EventCallback} from './gr-js-api-types';
suite('GrJsApiInterface tests', () => {
let element: GrJsApiInterface;
let plugin: Plugin;
let errorStub: SinonStub;
let pluginLoader: PluginLoader;
let sendStub: SinonStub;
let clock: SinonFakeTimers;
const throwErrFn = function () {
throw Error('Unfortunately, this handler has stopped');
};
setup(() => {
clock = useFakeTimers();
stubRestApi('getAccount').resolves({
name: 'Judy Hopps',
registered_on: '' as Timestamp,
});
sendStub = stubRestApi('send').resolves(
new Response(undefined, {status: 200})
);
pluginLoader = testResolver(pluginLoaderToken);
// We are using the jsApiService as the implementation class rather than the
// interface to better set up tests.
element = pluginLoader.jsApiService as GrJsApiInterface;
errorStub = stub(element.reporting, 'error');
pluginLoader.install(
p => {
// We are using the plugin API as the implementation class rather than
// the interface to better set up tests.
plugin = p as Plugin;
},
'0.1',
'http://test.com/plugins/testplugin/static/test.js'
);
testResolver(pluginLoaderToken).loadPlugins([]);
});
teardown(() => {
clock.restore();
element._removeEventCallbacks();
});
test('url', () => {
assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/');
assert.equal(
plugin.url('/static/test.js'),
'http://test.com/plugins/testplugin/static/test.js'
);
});
test('_send on failure rejects with response text', async () => {
sendStub.resolves({
status: 400,
text() {
return Promise.resolve('text');
},
});
const error = await assertFails<Error>(plugin._send(HttpMethod.POST, ''));
assert.equal(error.message, 'text');
});
test('_send on failure without text rejects with code', async () => {
sendStub.resolves({
status: 400,
text() {
return Promise.resolve(null);
},
});
const error = await assertFails<Error>(plugin._send(HttpMethod.POST, ''));
assert.equal(error.message, '400');
});
test('showchange event', async () => {
const showChangeStub = stub();
const testChange: ParsedChangeInfo = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
def: {...createRevision(), _number: 2 as RevisionPatchSetNum},
abc: {...createRevision(), _number: 1 as RevisionPatchSetNum},
},
};
const expectedChange = {mergeable: false, ...testChange};
plugin.on(EventType.SHOW_CHANGE, throwErrFn);
plugin.on(EventType.SHOW_CHANGE, showChangeStub);
element.handleShowChange({
change: testChange,
patchNum: 1 as PatchSetNum,
info: {mergeable: false},
});
await waitUntilCalled(showChangeStub, 'showChangeStub');
const [change, revision, info] = showChangeStub.firstCall.args;
assert.deepEqual(change, expectedChange);
assert.deepEqual(revision, testChange.revisions.abc);
assert.deepEqual(info, {mergeable: false});
assert.isTrue(errorStub.calledOnce);
});
test('show-revision-actions event', async () => {
const showRevisionActionsStub = stub();
const testChange: ChangeInfo = {
...createChange(),
_number: 42 as NumericChangeId,
revisions: {
def: {...createRevision(), _number: 2 as RevisionPatchSetNum},
abc: {...createRevision(), _number: 1 as RevisionPatchSetNum},
},
};
plugin.on(EventType.SHOW_REVISION_ACTIONS, throwErrFn);
plugin.on(EventType.SHOW_REVISION_ACTIONS, showRevisionActionsStub);
element.handleShowRevisionActions({
change: testChange,
revisionActions: {test: {}},
});
await waitUntilCalled(showRevisionActionsStub, 'showRevisionActionsStub');
const [actions, change] = showRevisionActionsStub.firstCall.args;
assert.deepEqual(change, testChange);
assert.deepEqual(actions, {test: {}});
assert.isTrue(errorStub.calledOnce);
});
test('handleShowChange awaits plugins load', async () => {
const testChange: ParsedChangeInfo = {
...createParsedChange(),
_number: 42 as NumericChangeId,
revisions: {
def: {...createRevision(), _number: 2 as RevisionPatchSetNum},
abc: {...createRevision(), _number: 1 as RevisionPatchSetNum},
},
};
const showChangeStub = stub();
testResolver(pluginLoaderToken).loadPlugins(['plugins/test.js']);
plugin.on(EventType.SHOW_CHANGE, showChangeStub);
element.handleShowChange({
change: testChange,
patchNum: 1 as PatchSetNum,
info: {mergeable: null},
});
assert.isFalse(showChangeStub.called);
// Timeout on loading plugins
clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
await waitEventLoop();
assert.isTrue(showChangeStub.called);
});
test('revert event', () => {
function appendToRevertMsg(
_c: unknown,
revertMsg: string,
originalMsg: string
) {
return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
}
const change = createChange();
assert.equal(element.modifyRevertMsg(change, 'test', 'origTest'), 'test');
assert.equal(errorStub.callCount, 0);
plugin.on(EventType.REVERT, throwErrFn);
plugin.on(EventType.REVERT, appendToRevertMsg);
assert.equal(
element.modifyRevertMsg(change, 'test', 'origTest'),
'test\n> origTest\ninfo'
);
assert.isTrue(errorStub.calledOnce);
plugin.on(EventType.REVERT, appendToRevertMsg);
assert.equal(
element.modifyRevertMsg(change, 'test', 'origTest'),
'test\n> origTest\ninfo\n> origTest\ninfo'
);
assert.isTrue(errorStub.calledTwice);
});
test('postrevert event labels', () => {
function getLabels(_c: unknown) {
return {'Code-Review': 1};
}
assert.deepEqual(element.getReviewPostRevert(undefined), {});
assert.equal(errorStub.callCount, 0);
plugin.on(EventType.POST_REVERT, throwErrFn);
plugin.on(EventType.POST_REVERT, getLabels);
assert.deepEqual(element.getReviewPostRevert(undefined), {
labels: {'Code-Review': 1},
});
assert.isTrue(errorStub.calledOnce);
});
test('postrevert event review', () => {
function getReview(_c: unknown) {
return {labels: {'Code-Review': 1}};
}
assert.deepEqual(element.getReviewPostRevert(undefined), {});
assert.equal(errorStub.callCount, 0);
plugin.on(EventType.POST_REVERT, throwErrFn);
plugin.on(EventType.POST_REVERT, getReview);
assert.deepEqual(element.getReviewPostRevert(undefined), {
labels: {'Code-Review': 1},
});
assert.isTrue(errorStub.calledOnce);
});
test('commitmsgedit event', async () => {
const commitMsgEditStub = stub();
const testMsg = 'Test CL commit message';
plugin.on(EventType.COMMIT_MSG_EDIT, throwErrFn);
plugin.on(EventType.COMMIT_MSG_EDIT, commitMsgEditStub);
element.handleCommitMessage(createChange(), testMsg);
await waitUntilCalled(commitMsgEditStub, 'commitMsgEditStub');
const msg = commitMsgEditStub.firstCall.args[1];
assert.deepEqual(msg, testMsg);
assert.isTrue(errorStub.calledOnce);
});
test('labelchange event', async () => {
const labelChangeStub = stub();
const testChange: ParsedChangeInfo = {
...createParsedChange(),
_number: 42 as NumericChangeId,
};
plugin.on(EventType.LABEL_CHANGE, throwErrFn);
plugin.on(EventType.LABEL_CHANGE, labelChangeStub);
element.handleLabelChange({change: testChange});
await waitUntilCalled(labelChangeStub, 'labelChangeStub');
const [change] = labelChangeStub.firstCall.args;
assert.deepEqual(change, testChange);
assert.isTrue(errorStub.calledOnce);
});
test('submitchange', () => {
plugin.on(EventType.SUBMIT_CHANGE, throwErrFn);
plugin.on(EventType.SUBMIT_CHANGE, () => true);
assert.isTrue(element.canSubmitChange(createChange()));
assert.isTrue(errorStub.calledOnce);
plugin.on(EventType.SUBMIT_CHANGE, () => false);
plugin.on(EventType.SUBMIT_CHANGE, () => true);
assert.isFalse(element.canSubmitChange(createChange()));
assert.isTrue(errorStub.calledTwice);
});
test('getLoggedIn', async () => {
// fake fetch for authCheck
stub(window, 'fetch').resolves(new Response(undefined, {status: 204}));
const loggedIn = await plugin.restApi().getLoggedIn();
assert.isTrue(loggedIn);
});
test('attributeHelper', () => {
assert.isOk(plugin.attributeHelper(document.createElement('div')));
});
test('getAdminMenuLinks', () => {
const links = [
{text: 'a', url: 'b'},
{text: 'c', url: 'd'},
];
// getAdminMenuLinks expects _getEventCallbacks to really return
// GrAdminApi[] even though _getEventCallbacks has return type
// EventCallback[]. Therefore this test must also return GrAdminApi[]
// disguised as EventCallback[].
const getCallbacksStub = stub(element, '_getEventCallbacks').returns([
{getMenuLinks: () => [links[0]]},
{getMenuLinks: () => [links[1]]},
] as unknown as EventCallback[]);
const result = element.getAdminMenuLinks();
assert.deepEqual(result, links);
assert.isTrue(getCallbacksStub.calledOnce);
assert.equal(getCallbacksStub.lastCall.args[0], EventType.ADMIN_MENU_LINKS);
});
suite('test plugin with base url', () => {
let baseUrlPlugin: Plugin;
setup(() => {
stubBaseUrl('/r');
pluginLoader.install(
p => {
// We are using the plugin API as the implementation class rather than
// the interface to better set up tests.
baseUrlPlugin = p as Plugin;
},
'0.1',
'http://test.com/r/plugins/baseurlplugin/static/test.js'
);
});
test('url', () => {
assert.notEqual(
baseUrlPlugin.url(),
'http://test.com/plugins/baseurlplugin/'
);
assert.equal(
baseUrlPlugin.url(),
'http://test.com/r/plugins/baseurlplugin/'
);
assert.equal(
baseUrlPlugin.url('/static/test.js'),
'http://test.com/r/plugins/baseurlplugin/static/test.js'
);
});
});
suite('popup', () => {
test('popup(moduleName) creates popup with component', () => {
const openStub = stub(GrPopupInterface.prototype, 'open').callsFake(
async function (this: GrPopupInterface) {
// Arrow function can't be used here, because we want to
// get properties from the instance of GrPopupInterface
assert.equal(this.plugin, plugin);
assert.equal(this.moduleName, 'some-name');
return this;
}
);
plugin.popup('some-name');
assert.isTrue(openStub.calledOnce);
});
});
suite('screen', () => {
test('screenUrl()', () => {
stubBaseUrl('/base');
assert.equal(plugin.screenUrl(), `${location.origin}/base/x/testplugin`);
assert.equal(
plugin.screenUrl('foo'),
`${location.origin}/base/x/testplugin/foo`
);
});
test('works', () => {
const registerCustomComponentStub = stub(
plugin,
'registerCustomComponent'
);
plugin.screen('foo', 'some-module');
assert.isTrue(
registerCustomComponentStub.calledWith(
'testplugin-screen-foo',
'some-module'
)
);
});
});
});