blob: fd784796450ac1e34480d9a2a44fc1cdb1e6d973 [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {fixture, html} from '@open-wc/testing-helpers';
import {IronDropdownElement} from '@polymer/iron-dropdown';
import {
BulkActionsModel,
bulkActionsModelToken,
} from '../../../models/bulk-actions/bulk-actions-model';
import {wrapInProvider} from '../../../models/di-provider-element';
import {getAppContext} from '../../../services/app-context';
import '../../../test/common-test-setup-karma';
import {createChange} from '../../../test/test-data-generators';
import {
MockPromise,
mockPromise,
queryAll,
queryAndAssert,
stubRestApi,
waitUntil,
waitUntilCalled,
waitUntilObserved,
} from '../../../test/test-utils';
import {ChangeInfo, NumericChangeId, TopicName} from '../../../types/common';
import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
import {GrButton} from '../../shared/gr-button/gr-button';
import './gr-change-list-topic-flow';
import type {GrChangeListTopicFlow} from './gr-change-list-topic-flow';
suite('gr-change-list-topic-flow tests', () => {
let element: GrChangeListTopicFlow;
let model: BulkActionsModel;
async function selectChange(change: ChangeInfo) {
model.addSelectedChangeNum(change._number);
await waitUntilObserved(model.selectedChanges$, selected =>
selected.some(other => other._number === change._number)
);
await element.updateComplete;
}
suite('dropdown closed', () => {
const changes: ChangeInfo[] = [
{
...createChange(),
_number: 1 as NumericChangeId,
subject: 'Subject 1',
},
{
...createChange(),
_number: 2 as NumericChangeId,
subject: 'Subject 2',
},
];
setup(async () => {
stubRestApi('getDetailedChangesWithActions').resolves(changes);
model = new BulkActionsModel(getAppContext().restApiService);
model.sync(changes);
element = (
await fixture(
wrapInProvider(
html`<gr-change-list-topic-flow></gr-change-list-topic-flow>`,
bulkActionsModelToken,
model
)
)
).querySelector('gr-change-list-topic-flow')!;
await selectChange(changes[0]);
await selectChange(changes[1]);
await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
await element.updateComplete;
});
test('skips dropdown render when closed', async () => {
expect(element).shadowDom.to.equal(/* HTML */ `
<gr-button
id="start-flow"
flatten=""
aria-disabled="false"
role="button"
tabindex="0"
>Topic</gr-button
>
<iron-dropdown
aria-disabled="false"
aria-hidden="true"
style="outline: none; display: none;"
vertical-align="auto"
horizontal-align="auto"
>
</iron-dropdown>
`);
});
test('dropdown hidden before flow button clicked', async () => {
const dropdown = queryAndAssert<IronDropdownElement>(
element,
'iron-dropdown'
);
assert.isFalse(dropdown.opened);
});
test('flow button click shows dropdown', async () => {
const button = queryAndAssert<GrButton>(element, 'gr-button#start-flow');
button.click();
await element.updateComplete;
const dropdown = queryAndAssert<IronDropdownElement>(
element,
'iron-dropdown'
);
assert.isTrue(dropdown.opened);
});
test('flow button click when open hides dropdown', async () => {
queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
await waitUntil(() =>
Boolean(
queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
)
);
queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
await waitUntil(
() =>
!queryAndAssert<IronDropdownElement>(element, 'iron-dropdown').opened
);
});
});
suite('changes in existing topics', () => {
const changesWithTopics: ChangeInfo[] = [
{
...createChange(),
_number: 1 as NumericChangeId,
subject: 'Subject 1',
topic: 'topic1' as TopicName,
},
{
...createChange(),
_number: 2 as NumericChangeId,
subject: 'Subject 2',
topic: 'topic2' as TopicName,
},
];
let setChangeTopicPromises: MockPromise<string>[];
let setChangeTopicStub: sinon.SinonStub;
async function resolvePromises() {
setChangeTopicPromises[0].resolve('foo');
setChangeTopicPromises[1].resolve('foo');
await element.updateComplete;
}
setup(async () => {
stubRestApi('getDetailedChangesWithActions').resolves(changesWithTopics);
setChangeTopicPromises = [];
setChangeTopicStub = stubRestApi('setChangeTopic');
for (let i = 0; i < changesWithTopics.length; i++) {
const promise = mockPromise<string>();
setChangeTopicPromises.push(promise);
setChangeTopicStub
.withArgs(changesWithTopics[i]._number, sinon.match.any)
.returns(promise);
}
model = new BulkActionsModel(getAppContext().restApiService);
model.sync(changesWithTopics);
element = (
await fixture(
wrapInProvider(
html`<gr-change-list-topic-flow></gr-change-list-topic-flow>`,
bulkActionsModelToken,
model
)
)
).querySelector('gr-change-list-topic-flow')!;
// select changes
await selectChange(changesWithTopics[0]);
await selectChange(changesWithTopics[1]);
await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
await element.updateComplete;
// open flow
queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
await element.updateComplete;
await flush();
});
test('renders existing-topics flow', () => {
expect(element).shadowDom.to.equal(
/* HTML */ `
<gr-button
id="start-flow"
flatten=""
aria-disabled="false"
role="button"
tabindex="0"
>Topic</gr-button
>
<iron-dropdown
aria-disabled="false"
vertical-align="auto"
horizontal-align="auto"
>
<div slot="dropdown-content">
<div class="chips">
<span role="button" aria-label="topic1" class="chip"
>topic1</span
>
<span role="button" aria-label="topic2" class="chip"
>topic2</span
>
</div>
<div class="footer">
<div class="loadingOrError"></div>
<div class="buttons">
<gr-button
id="apply-to-all-button"
flatten=""
aria-disabled="true"
disabled=""
role="button"
tabindex="-1"
>Apply to all</gr-button
>
<gr-button
id="remove-topics-button"
flatten=""
aria-disabled="true"
disabled=""
role="button"
tabindex="-1"
>Remove</gr-button
>
</div>
</div>
</div>
</iron-dropdown>
`,
{
// iron-dropdown sizing seems to vary between local & CI
ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
}
);
});
test('remove single topic', async () => {
queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
await element.updateComplete;
queryAndAssert<GrButton>(element, '#remove-topics-button').click();
await element.updateComplete;
assert.equal(
queryAndAssert(element, '.loadingText').textContent,
'Removing topic...'
);
await resolvePromises();
await element.updateComplete;
// not called for second change which as a different topic
assert.isTrue(setChangeTopicStub.calledOnce);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithTopics[0]._number,
'',
]);
});
test('remove multiple topics', async () => {
queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
queryAll<HTMLSpanElement>(element, 'span.chip')[1].click();
await element.updateComplete;
queryAndAssert<GrButton>(element, '#remove-topics-button').click();
await element.updateComplete;
assert.equal(
queryAndAssert(element, '.loadingText').textContent,
'Removing topics...'
);
await resolvePromises();
await element.updateComplete;
// not called for second change which as a different topic
assert.isTrue(setChangeTopicStub.calledTwice);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithTopics[0]._number,
'',
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithTopics[1]._number,
'',
]);
});
test('can only apply a single topic', async () => {
assert.isTrue(
queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
);
queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
await element.updateComplete;
assert.isFalse(
queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
);
queryAll<HTMLSpanElement>(element, 'span.chip')[1].click();
await element.updateComplete;
assert.isTrue(
queryAndAssert<GrButton>(element, '#apply-to-all-button').disabled
);
});
test('applies topic to all changes', async () => {
queryAll<HTMLSpanElement>(element, 'span.chip')[0].click();
await element.updateComplete;
queryAndAssert<GrButton>(element, '#apply-to-all-button').click();
await element.updateComplete;
assert.equal(
queryAndAssert(element, '.loadingText').textContent,
'Applying to all'
);
await resolvePromises();
await element.updateComplete;
assert.isTrue(setChangeTopicStub.calledTwice);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithTopics[0]._number,
'topic1',
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithTopics[1]._number,
'topic1',
]);
});
});
suite('change have no existing topics', () => {
const changesWithNoTopics: ChangeInfo[] = [
{
...createChange(),
_number: 1 as NumericChangeId,
subject: 'Subject 1',
},
{
...createChange(),
_number: 2 as NumericChangeId,
subject: 'Subject 2',
},
];
let setChangeTopicPromises: MockPromise<string>[];
let setChangeTopicStub: sinon.SinonStub;
async function resolvePromises() {
setChangeTopicPromises[0].resolve('foo');
setChangeTopicPromises[1].resolve('foo');
await element.updateComplete;
}
setup(async () => {
stubRestApi('getDetailedChangesWithActions').resolves(
changesWithNoTopics
);
setChangeTopicPromises = [];
setChangeTopicStub = stubRestApi('setChangeTopic');
for (let i = 0; i < changesWithNoTopics.length; i++) {
const promise = mockPromise<string>();
setChangeTopicPromises.push(promise);
setChangeTopicStub
.withArgs(changesWithNoTopics[i]._number, sinon.match.any)
.returns(promise);
}
model = new BulkActionsModel(getAppContext().restApiService);
model.sync(changesWithNoTopics);
element = (
await fixture(
wrapInProvider(
html`<gr-change-list-topic-flow></gr-change-list-topic-flow>`,
bulkActionsModelToken,
model
)
)
).querySelector('gr-change-list-topic-flow')!;
// select changes
await selectChange(changesWithNoTopics[0]);
await selectChange(changesWithNoTopics[1]);
await waitUntilObserved(model.selectedChanges$, s => s.length === 2);
await element.updateComplete;
// open flow
queryAndAssert<GrButton>(element, 'gr-button#start-flow').click();
await element.updateComplete;
await flush();
});
test('renders no-existing-topics flow', () => {
expect(element).shadowDom.to.equal(
/* HTML */ `
<gr-button
id="start-flow"
flatten=""
aria-disabled="false"
role="button"
tabindex="0"
>Topic</gr-button
>
<iron-dropdown
aria-disabled="false"
vertical-align="auto"
horizontal-align="auto"
>
<div slot="dropdown-content">
<gr-autocomplete
placeholder="Type topic name to create or filter topics"
show-blue-focus-border=""
></gr-autocomplete>
<div class="footer">
<div class="loadingOrError"></div>
<div class="buttons">
<gr-button
id="create-new-topic-button"
flatten=""
aria-disabled="true"
disabled=""
role="button"
tabindex="-1"
>Create new topic</gr-button
>
<gr-button
id="apply-topic-button"
flatten=""
aria-disabled="true"
disabled=""
role="button"
tabindex="-1"
>Apply</gr-button
>
</div>
</div>
</div>
</iron-dropdown>
`,
{
// iron-dropdown sizing seems to vary between local & CI
ignoreAttributes: [{tags: ['iron-dropdown'], attributes: ['style']}],
}
);
});
test('create new topic', async () => {
const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves(
[]
);
const autocomplete = queryAndAssert<GrAutocomplete>(
element,
'gr-autocomplete'
);
autocomplete.focus();
autocomplete.text = 'foo';
await element.updateComplete;
await waitUntilCalled(getTopicsStub, 'getTopicsStub');
assert.isTrue(
queryAndAssert<GrButton>(element, '#apply-topic-button').disabled
);
queryAndAssert<GrButton>(element, '#create-new-topic-button').click();
await element.updateComplete;
assert.equal(
queryAndAssert(element, '.loadingText').textContent,
'Creating topic...'
);
await resolvePromises();
await element.updateComplete;
assert.isTrue(setChangeTopicStub.calledTwice);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithNoTopics[0]._number,
'foo',
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithNoTopics[1]._number,
'foo',
]);
});
test('apply topic', async () => {
const getTopicsStub = stubRestApi('getChangesWithSimilarTopic').resolves([
{...createChange(), topic: 'foo' as TopicName},
]);
const autocomplete = queryAndAssert<GrAutocomplete>(
element,
'gr-autocomplete'
);
autocomplete.focus();
autocomplete.text = 'foo';
await element.updateComplete;
await waitUntilCalled(getTopicsStub, 'getTopicsStub');
assert.isTrue(
queryAndAssert<GrButton>(element, '#create-new-topic-button').disabled
);
queryAndAssert<GrButton>(element, '#apply-topic-button').click();
await element.updateComplete;
assert.equal(
queryAndAssert(element, '.loadingText').textContent,
'Applying topic...'
);
await resolvePromises();
assert.isTrue(setChangeTopicStub.calledTwice);
assert.deepEqual(setChangeTopicStub.firstCall.args, [
changesWithNoTopics[0]._number,
'foo',
]);
assert.deepEqual(setChangeTopicStub.secondCall.args, [
changesWithNoTopics[1]._number,
'foo',
]);
});
});
});