/** * @license * Copyright (C) 2020 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. */ import '../../../test/common-test-setup-karma'; import '../../core/gr-router/gr-router'; import './gr-change-metadata'; import {GerritNav} from '../../core/gr-navigation/gr-navigation'; import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader'; import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit'; import {GrChangeMetadata} from './gr-change-metadata'; import { createServerInfo, createUserConfig, createParsedChange, createAccountWithId, createRequirement, createCommitInfoWithRequiredCommit, createWebLinkInfo, createGerritInfo, createGitPerson, createCommit, createRevision, createAccountDetailWithId, createChangeConfig, } from '../../../test/test-data-generators'; import { ChangeStatus, SubmitType, RequirementStatus, GpgKeyInfoStatus, } from '../../../constants/constants'; import { EmailAddress, AccountId, CommitId, ServerInfo, RevisionInfo, ParentCommitInfo, TopicName, ElementPropertyDeepChange, PatchSetNum, NumericChangeId, LabelValueToDescriptionMap, Hashtag, } from '../../../types/common'; import {SinonStubbedMember} from 'sinon/pkg/sinon-esm'; import {RestApiService} from '../../../services/gr-rest-api/gr-rest-api'; import {tap} from '@polymer/iron-test-helpers/mock-interactions'; import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label'; import {PluginApi} from '../../plugins/gr-plugin-types'; import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator'; import {stubRestApi} from '../../../test/test-utils.js'; import {ParsedChangeInfo} from '../../../types/types'; const basicFixture = fixtureFromElement('gr-change-metadata'); const pluginApi = _testOnly_initGerritPluginApi(); suite('gr-change-metadata tests', () => { let element: GrChangeMetadata; setup(() => { stubRestApi('getLoggedIn').returns(Promise.resolve(false)); stubRestApi('getConfig').returns( Promise.resolve({ ...createServerInfo(), user: { ...createUserConfig(), anonymous_coward_name: 'test coward name', }, }) ); element = basicFixture.instantiate(); }); test('computed fields', () => { assert.isFalse( element._computeHideStrategy({ ...createParsedChange(), status: ChangeStatus.NEW, }) ); assert.isTrue( element._computeHideStrategy({ ...createParsedChange(), status: ChangeStatus.MERGED, }) ); assert.isTrue( element._computeHideStrategy({ ...createParsedChange(), status: ChangeStatus.ABANDONED, }) ); assert.equal( element._computeStrategy({ ...createParsedChange(), submit_type: SubmitType.CHERRY_PICK, }), 'Cherry Pick' ); assert.equal( element._computeStrategy({ ...createParsedChange(), submit_type: SubmitType.REBASE_ALWAYS, }), 'Rebase Always' ); }); test('computed fields requirements', () => { assert.isFalse( element._computeShowRequirements({ ...createParsedChange(), status: ChangeStatus.MERGED, }) ); assert.isFalse( element._computeShowRequirements({ ...createParsedChange(), status: ChangeStatus.ABANDONED, }) ); // No labels and no requirements: submit status is useless assert.isFalse( element._computeShowRequirements({ ...createParsedChange(), status: ChangeStatus.NEW, labels: {}, }) ); // Work in Progress: submit status should be present assert.isTrue( element._computeShowRequirements({ ...createParsedChange(), status: ChangeStatus.NEW, labels: {}, work_in_progress: true, }) ); // We have at least one reason to display Submit Status assert.isTrue( element._computeShowRequirements({ ...createParsedChange(), status: ChangeStatus.NEW, labels: { Verified: { approved: createAccountWithId(), }, }, requirements: [], }) ); assert.isTrue( element._computeShowRequirements({ ...createParsedChange(), status: ChangeStatus.NEW, labels: {}, requirements: [ { ...createRequirement(), fallbackText: 'Resolve all comments', status: RequirementStatus.OK, }, ], }) ); }); test('show strategy for open change', () => { element.change = { ...createParsedChange(), status: ChangeStatus.NEW, submit_type: SubmitType.CHERRY_PICK, labels: {}, }; flush(); const strategy = element.shadowRoot?.querySelector('.strategy'); assert.ok(strategy); assert.isFalse(strategy?.hasAttribute('hidden')); assert.equal(strategy?.children[1].innerHTML, 'Cherry Pick'); }); test('hide strategy for closed change', () => { element.change = { ...createParsedChange(), status: ChangeStatus.MERGED, labels: {}, }; flush(); assert.isTrue( element.shadowRoot?.querySelector('.strategy')?.hasAttribute('hidden') ); }); test('weblinks use GerritNav interface', () => { const weblinksStub = sinon .stub(GerritNav, '_generateWeblinks') .returns([{name: 'stubb', url: '#s'}]); element.commitInfo = createCommitInfoWithRequiredCommit(); element.serverConfig = createServerInfo(); flush(); const webLinks = element.$.webLinks; assert.isTrue(weblinksStub.called); assert.isFalse(webLinks.hasAttribute('hidden')); assert.equal(element._computeWebLinks(element.commitInfo)?.length, 1); }); test('weblinks hidden when no weblinks', () => { element.commitInfo = createCommitInfoWithRequiredCommit(); element.serverConfig = createServerInfo(); flush(); const webLinks = element.$.webLinks; assert.isTrue(webLinks.hasAttribute('hidden')); }); test('weblinks hidden when only gitiles weblink', () => { element.commitInfo = { ...createCommitInfoWithRequiredCommit(), web_links: [{...createWebLinkInfo(), name: 'gitiles', url: '#'}], }; element.serverConfig = createServerInfo(); flush(); const webLinks = element.$.webLinks; assert.isTrue(webLinks.hasAttribute('hidden')); assert.equal(element._computeWebLinks(element.commitInfo), null); }); test('weblinks hidden when sole weblink is set as primary', () => { const browser = 'browser'; element.commitInfo = { ...createCommitInfoWithRequiredCommit(), web_links: [{...createWebLinkInfo(), name: browser, url: '#'}], }; element.serverConfig = { ...createServerInfo(), gerrit: { ...createGerritInfo(), primary_weblink_name: browser, }, }; flush(); const webLinks = element.$.webLinks; assert.isTrue(webLinks.hasAttribute('hidden')); }); test('weblinks are visible when other weblinks', () => { const router = document.createElement('gr-router'); sinon .stub(GerritNav, '_generateWeblinks') .callsFake(router._generateWeblinks.bind(router)); element.commitInfo = { ...createCommitInfoWithRequiredCommit(), web_links: [{...createWebLinkInfo(), name: 'test', url: '#'}], }; flush(); const webLinks = element.$.webLinks; assert.isFalse(webLinks.hasAttribute('hidden')); assert.equal(element._computeWebLinks(element.commitInfo)?.length, 1); // With two non-gitiles weblinks, there are two returned. element.commitInfo = { ...createCommitInfoWithRequiredCommit(), web_links: [ {...createWebLinkInfo(), name: 'test', url: '#'}, {...createWebLinkInfo(), name: 'test2', url: '#'}, ], }; assert.equal(element._computeWebLinks(element.commitInfo)?.length, 2); }); test('weblinks are visible when gitiles and other weblinks', () => { const router = document.createElement('gr-router'); sinon .stub(GerritNav, '_generateWeblinks') .callsFake(router._generateWeblinks.bind(router)); element.commitInfo = { ...createCommitInfoWithRequiredCommit(), web_links: [ {...createWebLinkInfo(), name: 'test', url: '#'}, {...createWebLinkInfo(), name: 'gitiles', url: '#'}, ], }; flush(); const webLinks = element.$.webLinks; assert.isFalse(webLinks.hasAttribute('hidden')); // Only the non-gitiles weblink is returned. assert.equal(element._computeWebLinks(element.commitInfo)?.length, 1); }); suite('_getNonOwnerRole', () => { let change: ParsedChangeInfo | undefined; setup(() => { change = { ...createParsedChange(), owner: { ...createAccountWithId(), email: 'abc@def' as EmailAddress, _account_id: 1019328 as AccountId, }, revisions: { rev1: { ...createRevision(), uploader: { ...createAccountWithId(), email: 'ghi@def' as EmailAddress, _account_id: 1011123 as AccountId, }, commit: { ...createCommit(), author: {...createGitPerson(), email: 'jkl@def'}, committer: {...createGitPerson(), email: 'ghi@def'}, }, }, }, current_revision: 'rev1' as CommitId, }; }); suite('role=uploader', () => { test('_getNonOwnerRole for uploader', () => { assert.deepEqual( element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER), { ...createAccountWithId(), email: 'ghi@def' as EmailAddress, _account_id: 1011123 as AccountId, } ); }); test('_getNonOwnerRole that it does not return uploader', () => { // Set the uploader email to be the same as the owner. change!.revisions.rev1.uploader!._account_id = 1019328 as AccountId; assert.isNotOk( element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER) ); }); test('_computeShowRoleClass show uploader', () => { assert.equal( element._computeShowRoleClass(change, element._CHANGE_ROLE.UPLOADER), '' ); }); test('_computeShowRoleClass hide uploader', () => { // Set the uploader email to be the same as the owner. change!.revisions.rev1.uploader!._account_id = 1019328 as AccountId; assert.equal( element._computeShowRoleClass(change, element._CHANGE_ROLE.UPLOADER), 'hideDisplay' ); }); }); suite('role=committer', () => { test('_getNonOwnerRole for committer', () => { change!.revisions.rev1.uploader!.email = 'ghh@def' as EmailAddress; assert.deepEqual( element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER), {...createGitPerson(), email: 'ghi@def'} ); }); test('_getNonOwnerRole is null if committer is same as uploader', () => { assert.isNotOk( element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER) ); }); test('_getNonOwnerRole that it does not return committer', () => { // Set the committer email to be the same as the owner. change!.revisions.rev1.commit!.committer.email = 'abc@def'; assert.isNotOk( element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER) ); }); test('_getNonOwnerRole null for committer with no commit', () => { delete change!.revisions.rev1.commit; assert.isNotOk( element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER) ); }); }); suite('role=author', () => { test('_getNonOwnerRole for author', () => { assert.deepEqual( element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR), {...createGitPerson(), email: 'jkl@def'} ); }); test('_getNonOwnerRole that it does not return author', () => { // Set the author email to be the same as the owner. change!.revisions.rev1.commit!.author.email = 'abc@def'; assert.isNotOk( element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR) ); }); test('_getNonOwnerRole null for author with no commit', () => { delete change!.revisions.rev1.commit; assert.isNotOk( element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR) ); }); }); }); suite('Push Certificate Validation', () => { let serverConfig: ServerInfo | undefined; let change: ParsedChangeInfo | undefined; setup(() => { serverConfig = { ...createServerInfo(), receive: { enable_signed_push: 'true', }, }; change = { ...createParsedChange(), revisions: { rev1: { ...createRevision(1), push_certificate: { certificate: 'Push certificate', key: { status: GpgKeyInfoStatus.BAD, problems: ['No public keys found for key ID E5E20E52'], }, }, }, }, current_revision: 'rev1' as CommitId, status: ChangeStatus.NEW, labels: {}, mergeable: true, }; }); test('Push Certificate Validation test BAD', () => { change!.revisions.rev1!.push_certificate = { certificate: 'Push certificate', key: { status: GpgKeyInfoStatus.BAD, problems: ['No public keys found for key ID E5E20E52'], }, }; const result = element._computePushCertificateValidation( serverConfig, change ); assert.equal( result?.message, 'Push certificate is invalid:\n' + 'No public keys found for key ID E5E20E52' ); assert.equal(result?.icon, 'gr-icons:close'); assert.equal(result?.class, 'invalid'); }); test('Push Certificate Validation test TRUSTED', () => { change!.revisions.rev1!.push_certificate = { certificate: 'Push certificate', key: { status: GpgKeyInfoStatus.TRUSTED, }, }; const result = element._computePushCertificateValidation( serverConfig, change ); assert.equal( result?.message, 'Push certificate is valid and key is trusted' ); assert.equal(result?.icon, 'gr-icons:check'); assert.equal(result?.class, 'trusted'); }); test('Push Certificate Validation is missing test', () => { change!.revisions.rev1! = createRevision(1); const result = element._computePushCertificateValidation( serverConfig, change ); assert.equal( result?.message, 'This patch set was created without a push certificate' ); assert.equal(result?.icon, 'gr-icons:help'); assert.equal(result?.class, 'help'); }); }); test('_computeParents', () => { const parents: ParentCommitInfo[] = [ {...createCommit(), commit: '123' as CommitId, subject: 'abc'}, ]; const revision: RevisionInfo = { ...createRevision(1), commit: {...createCommit(), parents}, }; assert.equal(element._computeParents(undefined, revision), parents); const change = (current_revision: CommitId): ParsedChangeInfo => { return { ...createParsedChange(), current_revision, revisions: {456: revision}, }; }; const change_bad_revision = change('789' as CommitId); assert.deepEqual( element._computeParents(change_bad_revision, createRevision()), [] ); const change_no_commit: ParsedChangeInfo = { ...createParsedChange(), current_revision: '456' as CommitId, revisions: {456: createRevision()}, }; assert.deepEqual(element._computeParents(change_no_commit, undefined), []); const change_good = change('456' as CommitId); assert.equal(element._computeParents(change_good, undefined), parents); }); test('_currentParents', () => { const revision = (parent: CommitId): RevisionInfo => { return { ...createRevision(), commit: { ...createCommit(), parents: [{...createCommit(), commit: parent, subject: 'abc'}], }, }; }; element.change = { ...createParsedChange(), current_revision: '456' as CommitId, revisions: {456: revision('111' as CommitId)}, owner: {}, }; element.revision = revision('222' as CommitId); assert.equal(element._currentParents[0].commit, '222'); element.revision = revision('333' as CommitId); assert.equal(element._currentParents[0].commit, '333'); element.revision = undefined; assert.equal(element._currentParents[0].commit, '111'); element.change = createParsedChange(); assert.deepEqual(element._currentParents, []); }); test('_computeParentsLabel', () => { const parent: ParentCommitInfo = { ...createCommit(), commit: 'abc123' as CommitId, subject: 'My parent commit', }; assert.equal(element._computeParentsLabel([parent]), 'Parent'); assert.equal(element._computeParentsLabel([parent, parent]), 'Parents'); }); test('_computeParentListClass', () => { const parent: ParentCommitInfo = { ...createCommit(), commit: 'abc123' as CommitId, subject: 'My parent commit', }; assert.equal( element._computeParentListClass([parent], true), 'parentList nonMerge current' ); assert.equal( element._computeParentListClass([parent], false), 'parentList nonMerge notCurrent' ); assert.equal( element._computeParentListClass([parent, parent], false), 'parentList merge notCurrent' ); assert.equal( element._computeParentListClass([parent, parent], true), 'parentList merge current' ); }); test('_showAddTopic', () => { const changeRecord: ElementPropertyDeepChange< GrChangeMetadata, 'change' > = { base: {...createParsedChange()}, path: '', value: undefined, }; assert.isTrue(element._showAddTopic(undefined, false)); assert.isTrue(element._showAddTopic(changeRecord, false)); assert.isFalse(element._showAddTopic(changeRecord, true)); changeRecord.base!.topic = 'foo' as TopicName; assert.isFalse(element._showAddTopic(changeRecord, true)); assert.isFalse(element._showAddTopic(changeRecord, false)); }); test('_showTopicChip', () => { const changeRecord: ElementPropertyDeepChange< GrChangeMetadata, 'change' > = { base: {...createParsedChange()}, path: '', value: undefined, }; assert.isFalse(element._showTopicChip(undefined, false)); assert.isFalse(element._showTopicChip(changeRecord, false)); assert.isFalse(element._showTopicChip(changeRecord, true)); changeRecord.base!.topic = 'foo' as TopicName; assert.isFalse(element._showTopicChip(changeRecord, true)); assert.isTrue(element._showTopicChip(changeRecord, false)); }); test('_showCherryPickOf', () => { const changeRecord: ElementPropertyDeepChange< GrChangeMetadata, 'change' > = { base: {...createParsedChange()}, path: '', value: undefined, }; assert.isFalse(element._showCherryPickOf(undefined)); assert.isFalse(element._showCherryPickOf(changeRecord)); changeRecord.base!.cherry_pick_of_change = 123 as NumericChangeId; changeRecord.base!.cherry_pick_of_patch_set = 1 as PatchSetNum; assert.isTrue(element._showCherryPickOf(changeRecord)); }); suite('Topic removal', () => { let change: ParsedChangeInfo; setup(() => { change = { ...createParsedChange(), actions: { topic: {enabled: false}, }, topic: 'the topic' as TopicName, status: ChangeStatus.NEW, submit_type: SubmitType.CHERRY_PICK, labels: { test: { all: [{_account_id: 1 as AccountId, name: 'bojack', value: 1}], default_value: 0, values: ([] as unknown) as LabelValueToDescriptionMap, }, }, removable_reviewers: [], }; }); test('_computeTopicReadOnly', () => { let mutable = false; assert.isTrue(element._computeTopicReadOnly(mutable, change)); mutable = true; assert.isTrue(element._computeTopicReadOnly(mutable, change)); change!.actions!.topic!.enabled = true; assert.isFalse(element._computeTopicReadOnly(mutable, change)); mutable = false; assert.isTrue(element._computeTopicReadOnly(mutable, change)); }); test('topic read only hides delete button', () => { element.account = createAccountDetailWithId(); element.change = change; flush(); const button = element! .shadowRoot!.querySelector('gr-linked-chip')! .shadowRoot!.querySelector('gr-button'); assert.isTrue(button?.hasAttribute('hidden')); }); test('topic not read only does not hide delete button', () => { element.account = createAccountDetailWithId(); change.actions!.topic!.enabled = true; element.change = change; flush(); const button = element! .shadowRoot!.querySelector('gr-linked-chip')! .shadowRoot!.querySelector('gr-button'); assert.isFalse(button?.hasAttribute('hidden')); }); }); suite('Hashtag removal', () => { let change: ParsedChangeInfo; setup(() => { change = { ...createParsedChange(), actions: { hashtags: {enabled: false}, }, hashtags: ['test-hashtag' as Hashtag], labels: { test: { all: [{_account_id: 1 as AccountId, name: 'bojack', value: 1}], default_value: 0, values: ([] as unknown) as LabelValueToDescriptionMap, }, }, removable_reviewers: [], }; }); test('_computeHashtagReadOnly', () => { flush(); let mutable = false; assert.isTrue(element._computeHashtagReadOnly(mutable, change)); mutable = true; assert.isTrue(element._computeHashtagReadOnly(mutable, change)); change!.actions!.hashtags!.enabled = true; assert.isFalse(element._computeHashtagReadOnly(mutable, change)); mutable = false; assert.isTrue(element._computeHashtagReadOnly(mutable, change)); }); test('hashtag read only hides delete button', () => { flush(); element.account = createAccountDetailWithId(); element.change = change; flush(); const button = element! .shadowRoot!.querySelector('gr-linked-chip')! .shadowRoot!.querySelector('gr-button'); assert.isTrue(button?.hasAttribute('hidden')); }); test('hashtag not read only does not hide delete button', () => { flush(); element.account = createAccountDetailWithId(); change!.actions!.hashtags!.enabled = true; element.change = change; flush(); const button = element! .shadowRoot!.querySelector('gr-linked-chip')! .shadowRoot!.querySelector('gr-button'); assert.isFalse(button?.hasAttribute('hidden')); }); }); suite('remove reviewer votes', () => { setup(() => { sinon.stub(element, '_computeTopicReadOnly').returns(true); element.change = { ...createParsedChange(), topic: 'the topic' as TopicName, labels: { test: { all: [{_account_id: 1 as AccountId, name: 'bojack', value: 1}], default_value: 0, values: ([] as unknown) as LabelValueToDescriptionMap, }, }, removable_reviewers: [], }; flush(); }); suite('assignee field', () => { const dummyAccount = createAccountWithId(); const change: ParsedChangeInfo = { ...createParsedChange(), actions: { assignee: {enabled: false}, }, assignee: dummyAccount, }; let deleteStub: SinonStubbedMember; let setStub: SinonStubbedMember; setup(() => { deleteStub = stubRestApi('deleteAssignee'); setStub = stubRestApi('setAssignee'); element.serverConfig = { ...createServerInfo(), change: { ...createChangeConfig(), enable_assignee: true, }, }; }); test('changing change recomputes _assignee', () => { assert.isFalse(!!element._assignee?.length); const change = element.change; change!.assignee = dummyAccount; element._changeChanged(change); assert.deepEqual(element?._assignee?.[0], dummyAccount); }); test('modifying _assignee calls API', () => { assert.isFalse(!!element._assignee?.length); element.set('_assignee', [dummyAccount]); assert.isTrue(setStub.calledOnce); assert.deepEqual(element.change!.assignee, dummyAccount); element.set('_assignee', [dummyAccount]); assert.isTrue(setStub.calledOnce); element.set('_assignee', []); assert.isTrue(deleteStub.calledOnce); assert.equal(element.change!.assignee, undefined); element.set('_assignee', []); assert.isTrue(deleteStub.calledOnce); }); test('_computeAssigneeReadOnly', () => { let mutable = false; assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); mutable = true; assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); change.actions!.assignee!.enabled = true; assert.isFalse(element._computeAssigneeReadOnly(mutable, change)); mutable = false; assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); }); }); test('changing topic', () => { const newTopic = 'the new topic' as TopicName; const setChangeTopicStub = stubRestApi('setChangeTopic').returns( Promise.resolve(newTopic) ); element._handleTopicChanged(new CustomEvent('test', {detail: newTopic})); const topicChangedSpy = sinon.spy(); element.addEventListener('topic-changed', topicChangedSpy); assert.isTrue( setChangeTopicStub.calledWith(42 as NumericChangeId, newTopic) ); return setChangeTopicStub.lastCall.returnValue.then(() => { assert.equal(element.change!.topic, newTopic); assert.isTrue(topicChangedSpy.called); }); }); test('topic removal', () => { const newTopic = 'the new topic' as TopicName; const setChangeTopicStub = stubRestApi('setChangeTopic').returns( Promise.resolve(newTopic) ); const chip = element.shadowRoot!.querySelector('gr-linked-chip'); const remove = chip!.$.remove; const topicChangedSpy = sinon.spy(); element.addEventListener('topic-changed', topicChangedSpy); tap(remove); assert.isTrue(chip?.disabled); assert.isTrue(setChangeTopicStub.calledWith(42 as NumericChangeId)); return setChangeTopicStub.lastCall.returnValue.then(() => { assert.isFalse(chip?.disabled); assert.equal(element.change!.topic, '' as TopicName); assert.isTrue(topicChangedSpy.called); }); }); test('changing hashtag', () => { flush(); element._newHashtag = 'new hashtag' as Hashtag; const newHashtag: Hashtag[] = ['new hashtag' as Hashtag]; const setChangeHashtagStub = stubRestApi('setChangeHashtag').returns( Promise.resolve(newHashtag) ); element._handleHashtagChanged(); assert.isTrue( setChangeHashtagStub.calledWith(42 as NumericChangeId, { add: ['new hashtag' as Hashtag], }) ); return setChangeHashtagStub.lastCall.returnValue.then(() => { assert.equal(element.change!.hashtags, newHashtag); }); }); }); test('editTopic', () => { element.account = createAccountDetailWithId(); element.change = { ...createParsedChange(), actions: {topic: {enabled: true}}, }; flush(); const label = element.shadowRoot!.querySelector( '.topicEditableLabel' ) as GrEditableLabel; assert.ok(label); const openStub = sinon.stub(label, 'open'); element.editTopic(); flush(); assert.isTrue(openStub.called); }); suite('plugin endpoints', () => { test('endpoint params', done => { element.change = createParsedChange(); element.revision = createRevision(); interface MetadataGrEndpointDecorator extends GrEndpointDecorator { plugin: PluginApi; change: ParsedChangeInfo; revision: RevisionInfo; } let hookEl: MetadataGrEndpointDecorator; let plugin: PluginApi; pluginApi.install( p => { plugin = p; plugin .hook('change-metadata-item') .getLastAttached() .then(el => (hookEl = el as MetadataGrEndpointDecorator)); }, '0.1', 'http://some/plugins/url.html' ); getPluginLoader().loadPlugins([]); flush(() => { assert.strictEqual(hookEl!.plugin, plugin); assert.strictEqual(hookEl!.change, element.change); assert.strictEqual(hookEl!.revision, element.revision); done(); }); }); }); });